eclipse / microprofile-open-api

Microprofile open api
Apache License 2.0
131 stars 81 forks source link

[OAS 3.1.0] Schema changes #584

Closed Azquelt closed 2 months ago

Azquelt commented 5 months ago

OAS 3.1.0 changes from supporting most of an older JSON schema draft to supporting the whole of the JSON Schema 2020-12 draft Core and Validation.

We need to work out:

Tasks:

Azquelt commented 5 months ago

Here are my initial thoughts:

At a high level, the new JSON schema is very flexible. The core spec defines the idea of a "vocabulary" - a set of properties which have a documented meaning in a JSON schema - and a "dialect" - a set of vocabularies which are supported.

The core spec defines a core vocabulary, and two vocabularies for applying subschemas. The validation spec defines vocabularies for validating the structure and contents of a JSON document, a vocabulary for annotating a schema with metadata like a description and a dialect which includes all the required vocabularies of the core and validation specs.

The OpenAPI spec then defines its own vocabulary containing some extensions, and a dialect which requires the required JSON schema vocabularies and the OpenAPI vocabulary.

By default, all schemas use the dialect defined by OpenAPI, but they can also choose to use a different dialect by using the $schema property in their schema, or by setting jsonSchemaDialect at the top level which sets the default dialect for all schema objects in the document.

In theory, a user can write their OpenAPI document using any dialect of JSON schema they like, as long as they declare it in the document. If we want to be able to read any OpenAPI document the user might package in their application, our model would need to handle any arbitrary JSON document as a schema.

Azquelt commented 5 months ago

Here are the things that I think have changed in the schema between 3.0 and 3.1 that would need to be reflected in our model:

New fields (core)

Changed fields (core)

New fields (validation)

Changed fields (validation)

Removed fields

Additional

The following fields are new in the core schema, but I'm not sure if they're relevant to its use in OpenAPI:

I don't think users of OpenAPI are likely to want to use these fields, but we need to accommodate them anyway since they're valid so we must be able to read them from a user-supplied document.

However, since OpenAPI permits arbitrary dialects, our model may need to allow arbitrary JSON as the schema anyway. If we include a mechanism to allow arbitrary additional properties, we could say that these properties can only be set through that mechanism.

Azquelt commented 4 months ago

A few things I noticed while trying to implement this for smallrye:

  1. The semantics of nullable vs. having null in the list of types is subtly different. While nullable can betrue, false or unset, null is either in the list of types or it isn't. Also, nullable = true has no effect if type is not set. Whereas you could previously do this for an optional field:

    {
        "nullable": "true",
        "allOf": {
            "$ref": "..."
        }
    }

    With the new schema this requires an anyOf.

  2. The Extensible interface is now fairly clear throughout that it only works on properties beginning x-. addExtension can be interpreted as adding x- to the start of any key which doesn't already have that prefix. Having Extensible work this way allows for consistent handling of types which support extensions, so I think we should leave it as it is and create a new interface for "freeform" objects.

MikeEdgar commented 4 months ago

Wouldn't having null absent from the type array be semantically equivalent to nullable: false or undefined/unset?

For the second issue, I think omitting type entirely implies any type, including null. Basically, the value is unconstrained.

Azquelt commented 4 months ago

Yes, you can have a semanitcally equivalent end result, but it makes trying to keep existing code which uses the interface working difficult.

At the moment, I've deprecated setNullable and the single argument setType, thinking that they could both be implemented by manipulating the list of types.

However, you can't quite do that consistently. If the user calls setNullable but never calls setType, you don't want to end up with "type": ["null"] in the schema, since that forbids anything that's not null. However if they do call setType(OBJECT), you do want "type": ["null", "object"] so would need to store a flag somewhere to say that nullable has been requested.

Whether that's an issue or not depends on how you implement freeform objects. I tried making the Schema implementation a thin wrapper around a JSON object, but then you have nowhere to store data except within the JSON.