diff --git a/CHANGELOG.md b/CHANGELOG.md index c49b5296..7413bce5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ Request validation is called automatically for these operations. ### Fixes +- Fixed: Validating against a schema from a referenced file raised `ArgumentError` in OpenAPI 3.0 documents when a top-level key of that file collides with a JSON Schema keyword, such as `$ref: 'parameters.yaml#/id'`. The containing file is no longer parsed as a schema itself, so such keys work like any other now. See #348. - Fixed: `$ref`s nested inside the schema of a parameter or a response header are resolved now, so these values are unpacked and converted as described. Before, only a `$ref` at the top level of the schema was resolved. See #450. - Fixed: The JSON schema of a parameter that uses a `content` field with a `$ref`'d schema is resolved now. - Fixed: Loading a document no longer raises `NoMethodError` when a parameter has neither `schema` nor `content`. diff --git a/lib/openapi_first/ref_resolver.rb b/lib/openapi_first/ref_resolver.rb index aa3ff23d..f2f93902 100644 --- a/lib/openapi_first/ref_resolver.rb +++ b/lib/openapi_first/ref_resolver.rb @@ -84,7 +84,7 @@ def ==(_other) def resolve_ref(pointer) if pointer.start_with?('#') value = Hana::Pointer.new(pointer[1..]).eval(context) - raise "Unknown reference #{pointer} in #{context}" unless value + raise "Unknown reference #{pointer.inspect} in #{filepath || context}" unless value return ref_resolver.for(value, filepath:, context:) end @@ -93,7 +93,10 @@ def resolve_ref(pointer) full_path = File.expand_path(relative_path, dir) return ref_resolver.load(full_path) unless file_pointer - ref_resolver.file_at(full_path, file_pointer) + resolved = ref_resolver.file_at(full_path, file_pointer) + raise "Unknown reference #{pointer.inspect} in #{filepath || context}" unless resolved + + resolved rescue OpenapiFirst::FileNotFoundError => e message = "Problem with reference resolving #{pointer.inspect} in " \ "file #{File.absolute_path(filepath).inspect}: #{e.message}" @@ -173,6 +176,14 @@ def schema(options) class Schema extend Forwardable + # The root context is a document, not a schema. Parsing it with only the core vocabulary + # keeps document keys that collide with dialect keywords (like "id" in OpenAPI 3.0) + # from being parsed as such and keeps them navigable for $ref pointers. + DOCUMENT_META_SCHEMA = JSONSchemer::Schema.new( + {}, + vocabulary: { 'https://json-schema.org/draft/2020-12/vocab/core' => true } + ) + def initialize(value:, context:, base_uri:, options:) @value = value @context = context @@ -186,10 +197,24 @@ def initialize(value:, context:, base_uri:, options:) def schema @schema ||= begin - root_schema = JSONSchemer::Schema.new(context, base_uri:, **options) + root_schema = JSONSchemer::Schema.new(context, base_uri:, **options, meta_schema: DOCUMENT_META_SCHEMA) + apply_dialect(root_schema) JSONSchemer::Schema.new(value, nil, root_schema, base_uri:, **options) end end + + private + + # Set the dialect meta schema on the root like JSONSchemer::Schema#parse would, + # so that schemas resolved via $ref pointers into the document inherit it. + def apply_dialect(root_schema) + dialect = options[:meta_schema] || options.fetch(:configuration, JSONSchemer.configuration).meta_schema + if dialect.is_a?(String) + JSONSchemer::Schema::SCHEMA_KEYWORD_CLASS.new(dialect, root_schema, '$schema') + else + root_schema.meta_schema = dialect + end + end end # @visibility private diff --git a/spec/data/components/parameters.yaml b/spec/data/components/parameters.yaml new file mode 100644 index 00000000..2a7d18e7 --- /dev/null +++ b/spec/data/components/parameters.yaml @@ -0,0 +1,6 @@ +id: + name: id + in: path + required: true + schema: + type: integer diff --git a/spec/data/parameters.yaml b/spec/data/parameters.yaml index 093264eb..61c5ab24 100644 --- a/spec/data/parameters.yaml +++ b/spec/data/parameters.yaml @@ -1,4 +1,4 @@ -openapi: "3.0.2" +openapi: "3.0.3" info: version: 1.0.0 title: Search example @@ -92,11 +92,7 @@ paths: required: true schema: type: integer - - name: id - in: path - required: true - schema: - type: integer + - $ref: components/parameters.yaml#/id get: responses: "200": diff --git a/spec/middlewares/request_validation/query_parameter_validation_spec.rb b/spec/middlewares/request_validation/query_parameter_validation_spec.rb index c24ac37b..ab270976 100644 --- a/spec/middlewares/request_validation/query_parameter_validation_spec.rb +++ b/spec/middlewares/request_validation/query_parameter_validation_spec.rb @@ -134,6 +134,15 @@ hash_including('parameter' => '', 'code' => 'required') ) end + + it 'validates the path param' do + get '/stuff/abc' + expect(last_response.status).to eq(400) + errors = JSON.parse(last_response.body)['errors'] + expect(errors).to contain_exactly( + hash_including('parameter' => 'id', 'code' => 'integer') + ) + end end context 'with array query parameters' do