From c80d60f021b68cd78f45394bda315b78a21c9e87 Mon Sep 17 00:00:00 2001 From: Andreas Haller Date: Tue, 15 Apr 2025 20:53:34 +0200 Subject: [PATCH 1/3] Better error message if nested/ref was not found --- lib/openapi_first/ref_resolver.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/openapi_first/ref_resolver.rb b/lib/openapi_first/ref_resolver.rb index aa3ff23d..933a9fd0 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}" From 27c19dd50283fc0b3c1ece18104450688f5cf6a5 Mon Sep 17 00:00:00 2001 From: Andreas Haller Date: Tue, 15 Apr 2025 23:02:03 +0200 Subject: [PATCH 2/3] Reproduce issue #348 about $ref and OAD 3.0.x Related to https://github.com/ahx/openapi_first/issues/348 --- spec/data/components/parameters.yaml | 6 ++++++ spec/data/parameters.yaml | 8 ++------ .../query_parameter_validation_spec.rb | 9 +++++++++ 3 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 spec/data/components/parameters.yaml 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 From 33799ad98b4c18c982cd79d0df5e79d84b75ff19 Mon Sep 17 00:00:00 2001 From: Andreas Haller Date: Thu, 13 Aug 2026 23:59:47 +0200 Subject: [PATCH 3/3] Fix validating against a schema from a referenced file raised Related to #348 --- CHANGELOG.md | 1 + lib/openapi_first/ref_resolver.rb | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) 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 933a9fd0..f2f93902 100644 --- a/lib/openapi_first/ref_resolver.rb +++ b/lib/openapi_first/ref_resolver.rb @@ -176,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 @@ -189,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