biocad / openapi3

OpenAPI 3.0 data model
BSD 3-Clause "New" or "Revised" License
39 stars 53 forks source link

validateJSON semantics with `allOf` seem incorrect #93

Open pbrisbin opened 7 months ago

pbrisbin commented 7 months ago

I have a Haskell type that is, roughly:

data WithMetadata a m = WithMetadata a m

instance (ToJSON a, ToJSON m) => ToJSON (WithMetadata a m) where
  toJSON (WithMeta a m) = unionObjects (toJSON a) (toJSON m)

unionObjects :: Value -> Value -> Value
unionObjects = undefined

Basically it just says that { foo: bar } `WithMetadata` { baz: bat } encodes like { foo: bar, baz: bat }

Writing the ToSchema for this was surprisingly pleasant,

instance (ToSchema a, ToSchema m) => ToSchema (a `WithMetadata` m) where
  declareNamedSchema _ = do
    aSchema <- declareSchemaRef (Proxy @a)
    mSchema <- declareSchemaRef (Proxy @m)

    pure $
      NamedSchema Nothing $
        mempty
          & type_ ?~ OpenApiObject
          & allOf ?~ [aSchema, mSchema]
Schema and expanded references ```json { "items": { "allOf": [ { "$ref": "#/components/schemas/StandardSet" // this is the a }, { "properties": { // and this is the m "administrativeAreas": { "items": { "$ref": "#/components/schemas/CountryAdministrativeArea" }, "type": "array" } }, "required": [ "administrativeAreas" ], "type": "object" } ], "type": "object" }, "type": "array" } ``` ```json { "CountryAdministrativeArea": { "properties": { "administrativeArea": { "type": "string" }, "countryCode": { "$ref": "#/components/schemas/CountryCode" } }, "required": [ "countryCode" ], "type": "object" }, "CountryCode": { "type": "string" }, "StandardSet": { "properties": { "description": { "type": "string" }, "domainLabel": { "type": "string" }, "id": { "type": "string" }, "isLive": { "type": "boolean" }, "name": { "type": "string" }, "standardLabel": { "type": "string" } }, "required": [ "id", "name", "description", "domainLabel", "standardLabel", "isLive" ], "type": "object" } } ```

I don't claim to know the semantics of allOf, but the documentation came out exactly as I wanted: it shows me an object with all of the properties of a and m:

[
  {
    "description": "string", // these fields are the ToJSON of a
    "domainLabel": "string",
    "id": "string",
    "isLive": true,
    "name": "string",
    "standardLabel": "string",
    "administrativeAreas": [ // and this field is the ToJSON of m
      {
        "administrativeArea": "string",
        "countryCode": "string"
      }
    ]
  }
]

As well as a combined schema, as if I had written it by hand:

But then I tried to use Data.OpenApi.validateJSON with this schema and a valid example, and I get a failure on every single field being unexpected:

  Errors:
    - property "administrativeAreas" is found in JSON value, but it is not mentioned in Swagger schema
    - property "description" is found in JSON value, but it is not mentioned in Swagger schema
    - property "domainLabel" is found in JSON value, but it is not mentioned in Swagger schema
    - property "id" is found in JSON value, but it is not mentioned in Swagger schema
    - property "isLive" is found in JSON value, but it is not mentioned in Swagger schema
    - property "name" is found in JSON value, but it is not mentioned in Swagger schema
    - property "standardLabel" is found in JSON value, but it is not mentioned in Swagger schema

I think this comes from the implementation for allOf:

    (view allOf -> Just variants) -> do
      -- Default semantics for Validation Monad will abort when at least one
      -- variant does not match.
      forM_ variants $ \var ->
        validateWithSchemaRef var val

What is probably happening is it is validating each of the allOf schemas individually, so any properties appear as extra when validating any one of the other schemas that don't specify them.

I think (again, just based on what the Example and Schema docs show), the intent of allOf (at least with type: object) would be to union them all into one schema, then validate with that.

The comment says "'Default' semantics" -- is there some way I can get the semantics I expected instead?