From 3c948cdd9358d16bbba153cd615ede8e279ffa6f Mon Sep 17 00:00:00 2001 From: Samantha Voigt Date: Tue, 16 Jun 2026 12:36:22 -0700 Subject: [PATCH] Deserialize header values before validating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Header values are always transmitted as strings, but `Validators::Headers` validated the raw string against the header schema with no deserialization. Any header typed as `integer`, `number`, or `boolean` could therefore never validate — e.g. a `RateLimit-Limit: 300` response header failed against `{type: integer}` because "300" is a String. Deserialize the value with `OpenapiParameters::Converter` before validating — the same converter `Doc::Parameter` already uses for query parameters — so a header's value is coerced to the type its schema declares (per `style: simple`). Values that can't be parsed are left unchanged and still fail validation. Co-authored-by: Claude --- lib/openapi_contracts/validators/headers.rb | 3 +++ spec/fixtures/openapi/openapi.yaml | 4 ++++ .../validators/headers_spec.rb | 22 +++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/lib/openapi_contracts/validators/headers.rb b/lib/openapi_contracts/validators/headers.rb index 0796710..f373f1f 100644 --- a/lib/openapi_contracts/validators/headers.rb +++ b/lib/openapi_contracts/validators/headers.rb @@ -19,6 +19,9 @@ def validate if value.blank? @errors << "Missing header #{header.name}" if header.required? else + # Header values arrive as strings; deserialize to the schema's type + # (per `style: simple`) before validating, as Doc::Parameter does. + value = OpenapiParameters::Converter.convert(value, header.schema) schemer = JSONSchemer.schema(header.schema) unless schemer.valid?(value) validation_errors = schemer.validate(value).to_a diff --git a/spec/fixtures/openapi/openapi.yaml b/spec/fixtures/openapi/openapi.yaml index fe1d888..103cc78 100644 --- a/spec/fixtures/openapi/openapi.yaml +++ b/spec/fixtures/openapi/openapi.yaml @@ -97,6 +97,10 @@ paths: responses: '200': description: Ok + headers: + x-rate-limit: + schema: + type: integer content: application/json: schema: diff --git a/spec/openapi_contracts/validators/headers_spec.rb b/spec/openapi_contracts/validators/headers_spec.rb index 862ff00..a79e6f8 100644 --- a/spec/openapi_contracts/validators/headers_spec.rb +++ b/spec/openapi_contracts/validators/headers_spec.rb @@ -46,4 +46,26 @@ ] end end + + context 'with a non-string header type (style: simple)' do + include_context 'when using GET /pets' + + context 'when the value parses to the declared type' do + before { response_headers['x-rate-limit'] = '300' } + + it 'deserializes the value and has no errors' do + expect(subject.call).to be_empty + end + end + + context 'when the value does not parse to the declared type' do + before { response_headers['x-rate-limit'] = 'not-a-number' } + + it 'returns the error' do + expect(subject.call).to eq [ + 'Header x-rate-limit validation error: value at root is not an integer (value: not-a-number)' + ] + end + end + end end