openapi_first icon indicating copy to clipboard operation
openapi_first copied to clipboard

Cannot validate responses that use `oneOf` unions with `discriminator`.

Open moberegger opened this issue 7 months ago • 6 comments

Ran into an issue with oneOf unions, but only when using a discriminator property. I am not sure if this is an openapi_first issue or a json_schemer issue, so I'd though I'd try here first.

Given a simple YAML configuration like

---
openapi: 3.1.0
info:
  title: API V1
  version: v1
paths:
  "/pets":
    get:
      summary: Pets
      responses:
        '200':
          description: successful
          content:
            application/json:
              schema:
                items:
                  oneOf:
                    - '$ref': '#/components/schemas/Cat'
                    - '$ref': '#/components/schemas/Dog'
                  discriminator: 
                    propertyName: petType
                type: array
components:
  schemas:
    Cat:
      type: object
      properties:
        id: 
          type: integer
        petType: 
          type: string
          enum:
            - cat
        meow:
          type: string
    Dog:
      type: object
      properties:
        id: 
          type: integer
        petType: 
          type: string
          enum:
            - dog
        bark:
          type: string

and using the ResponseValidation middleware

config.middleware.use OpenapiFirst::Middlewares::ResponseValidation, spec: 'swagger/v1/swagger.yaml', raise_error: true

and responding with some simple hardcoded data

class PetsController < ApplicationController
  def index
    # Hardcode a response
    render json: [
      { id: 1, petType: 'cat', meow: 'Prrr' },
      { id: 2, petType: 'dog', bark: 'Woof' }
    ], status: :ok
  end
end

The following error occurs

{
    "status": 500,
    "error": "Internal Server Error",
    "exception": "#<KeyError: key not found: \"$ref\">",
    "traces": {
        "Application Trace": [],
        "Framework Trace": [
            {
                "exception_object_id": 9400,
                "id": 0,
                "trace": "json_schemer (2.3.0) lib/json_schemer/openapi31/vocab/base.rb:59:in `fetch'"
            },
            {
                "exception_object_id": 9400,
                "id": 1,
                "trace": "json_schemer (2.3.0) lib/json_schemer/openapi31/vocab/base.rb:59:in `block in subschemas_by_property_value'"
            },
            {
                "exception_object_id": 9400,
                "id": 2,
                "trace": "json_schemer (2.3.0) lib/json_schemer/openapi31/vocab/base.rb:58:in `each'"
            },
            {
                "exception_object_id": 9400,
                "id": 3,
                "trace": "json_schemer (2.3.0) lib/json_schemer/openapi31/vocab/base.rb:58:in `subschemas_by_property_value'"
            },
            {
                "exception_object_id": 9400,
                "id": 4,
                "trace": "json_schemer (2.3.0) lib/json_schemer/openapi31/vocab/base.rb:110:in `validate'"
            },
            {
                "exception_object_id": 9400,
                "id": 5,
                "trace": "json_schemer (2.3.0) lib/json_schemer/schema.rb:141:in `block in validate_instance'"
            },
            {
                "exception_object_id": 9400,
                "id": 6,
                "trace": "json_schemer (2.3.0) lib/json_schemer/schema.rb:140:in `each'"
            },
            {
                "exception_object_id": 9400,
                "id": 7,
                "trace": "json_schemer (2.3.0) lib/json_schemer/schema.rb:140:in `validate_instance'"
            },
            {
                "exception_object_id": 9400,
                "id": 8,
                "trace": "json_schemer (2.3.0) lib/json_schemer/draft202012/vocab/applicator.rb:184:in `block in validate'"
            },
            {
                "exception_object_id": 9400,
                "id": 9,
                "trace": "json_schemer (2.3.0) lib/json_schemer/draft202012/vocab/applicator.rb:183:in `map'"
            },
            {
                "exception_object_id": 9400,
                "id": 10,
                "trace": "json_schemer (2.3.0) lib/json_schemer/draft202012/vocab/applicator.rb:183:in `with_index'"
            },
            {
                "exception_object_id": 9400,
                "id": 11,
                "trace": "json_schemer (2.3.0) lib/json_schemer/draft202012/vocab/applicator.rb:183:in `validate'"
            },
            {
                "exception_object_id": 9400,
                "id": 12,
                "trace": "json_schemer (2.3.0) lib/json_schemer/schema.rb:141:in `block in validate_instance'"
            },
            {
                "exception_object_id": 9400,
                "id": 13,
                "trace": "json_schemer (2.3.0) lib/json_schemer/schema.rb:140:in `each'"
            },
            {
                "exception_object_id": 9400,
                "id": 14,
                "trace": "json_schemer (2.3.0) lib/json_schemer/schema.rb:140:in `validate_instance'"
            },
            {
                "exception_object_id": 9400,
                "id": 15,
                "trace": "json_schemer (2.3.0) lib/json_schemer/schema.rb:106:in `validate'"
            },
            {
                "exception_object_id": 9400,
                "id": 16,
                "trace": "openapi_first (2.1.0) lib/openapi_first/schema.rb:29:in `validate'"
            },
            {
                "exception_object_id": 9400,
                "id": 17,
                "trace": "openapi_first (2.1.0) lib/openapi_first/validators/response_body.rb:25:in `call'"
            },
            {
                "exception_object_id": 9400,
                "id": 18,
                "trace": "openapi_first (2.1.0) lib/openapi_first/response_validator.rb:22:in `block (2 levels) in call'"
            },
            {
                "exception_object_id": 9400,
                "id": 19,
                "trace": "openapi_first (2.1.0) lib/openapi_first/response_validator.rb:22:in `each'"
            },
            {
                "exception_object_id": 9400,
                "id": 20,
                "trace": "openapi_first (2.1.0) lib/openapi_first/response_validator.rb:22:in `block in call'"
            },
            {
                "exception_object_id": 9400,
                "id": 21,
                "trace": "openapi_first (2.1.0) lib/openapi_first/response_validator.rb:21:in `catch'"
            },
            {
                "exception_object_id": 9400,
                "id": 22,
                "trace": "openapi_first (2.1.0) lib/openapi_first/response_validator.rb:21:in `call'"
            },
            {
                "exception_object_id": 9400,
                "id": 23,
                "trace": "openapi_first (2.1.0) lib/openapi_first/response.rb:29:in `validate'"
            },
            {
                "exception_object_id": 9400,
                "id": 24,
                "trace": "openapi_first (2.1.0) lib/openapi_first/definition.rb:79:in `validate_response'"
            },
            {
                "exception_object_id": 9400,
                "id": 25,
                "trace": "openapi_first (2.1.0) lib/openapi_first/middlewares/response_validation.rb:28:in `call'"
            },
            {
                "exception_object_id": 9400,
                "id": 26,
                "trace": "openapi_first (2.1.0) lib/openapi_first/middlewares/request_validation.rb:36:in `call'"
            },

            # Snipped for brevity
        ]
    }
}

If I remove the discriminator property and just have

    Pet:
      oneOf:
        - '$ref': '#/components/schemas/Cat'
        - '$ref': '#/components/schemas/Dog'

Everything works.

[
    {
        "id": 1,
        "petType": "cat",
        "meow": "Prrr"
    },
    {
        "id": 2,
        "petType": "dog",
        "bark": "Woof"
    }
]

I dug into the openapi_first and json_schemer source a bit and what I think is happening is that json_schemer expects those $refs to be there, but openapi_first dereferences them out when creating the schemas. I can see a response schema being created with the spec doc loaded as

{"items"=>
  {"oneOf"=>
    [{"type"=>"object", "properties"=>{"id"=>{"type"=>"integer"}, "petType"=>{"type"=>"string", "enum"=>["cat"]}, "meow"=>{"type"=>"string"}}},
     {"type"=>"object", "properties"=>{"id"=>{"type"=>"integer"}, "petType"=>{"type"=>"string", "enum"=>["dog"]}, "bark"=>{"type"=>"string"}}}],
   "discriminator"=>{"propertyName"=>"petType"}},
 "type"=>"array"}

The presence of the discriminator seems to trigger json_schemer to look for the $refs. The tests seem to indicate that json_schemer supports oneOf unions, but they suggest that it needs to be aware of the entire schema (or at least the refs), where as openapi_first takes a modular approach and creates separate request and response schemas for each path.

Again, don't know if this is an openapi_first problem or a json_schemer problem. It does seem like json_schemer expects the entire specification to be given to it, which is not how openapi_first is currently using it. That being said, this is the only issue I have run into thus far in a project that is making heavy use of $refs, so perhaps it's a json_schemer bug with discriminators because that shouldn't assume that $refs are being used.

Perhaps instead of dereferencing, a ref map is created that can then be provided to json_schemer

schema = {
  '$id' => 'http://example.com/schema',
  'allOf' => [
    { '$ref' => 'schema/one' },
    { '$ref' => 'schema/two' }
  ]
}
refs = {
  URI('http://example.com/schema/one') => {
    'type' => 'integer'
  },
  URI('http://example.com/schema/two') => {
    'minimum' => 11
  }
}
schemer = JSONSchemer.schema(schema, :ref_resolver => refs.to_proc)

moberegger avatar Jul 22 '24 16:07 moberegger