Skip to content

fix(adapters): align adapter JSON content-type detection with ContentTypeMatcher#253

Merged
wadakatu merged 2 commits into
mainfrom
fix/non-json-content-type-issue-251
May 18, 2026
Merged

fix(adapters): align adapter JSON content-type detection with ContentTypeMatcher#253
wadakatu merged 2 commits into
mainfrom
fix/non-json-content-type-issue-251

Conversation

@wadakatu
Copy link
Copy Markdown
Collaborator

Summary

The three adapter body-extraction methods (Laravel extractRequestBody / extractJsonBody, Symfony extractSymfonyJsonBody) decided whether to JSON-decode a body using a loose str_contains(strtolower($contentType), 'json') check. They now use the strict ContentTypeMatcher::isJsonContentType() — the same check the body validators already use — so the adapter and validator agree on which media types count as JSON.

Why

Fixes #251.

The loose check treats borderline media types such as application/jsonsomethingweird as JSON, so the adapter would attempt json_decode() on a non-JSON body and fail with a misleading "could not be parsed as JSON" parse error, when the real issue is the Content-Type. After the change the non-JSON body is left undecoded and reported absent, and the validator's content negotiation surfaces its clean "Content-Type is not defined" diagnostic instead.

Scope note (investigation finding): Issue #251 describes a "silent pass" for non-empty non-JSON bodies. On close inspection that scenario no longer reproduces — the content negotiation introduced in #246 already makes the body validators decide non-JSON handling from the separately-forwarded $contentType (loud "Content-Type is not defined" when the spec does not declare the type; correctly accepted when it does). The body envelope's absent-vs-present bit is never consulted for non-JSON types, so extending DecodedBody with a third state was deliberately not done — it would be a dead state nothing reads. The remaining, actionable gap was the adapter/validator JSON-detection inconsistency addressed here.

Behavior change (minor, intentional): non-standard JSON-ish content types (application/jsonsomethingweird, text/json, application/jsonp) are now treated as non-JSON by the adapters, matching the validators. Standard types (application/json, application/*+json, application/json; charset=utf-8) are unaffected.

Verification

Failing tests were added first (TDD), confirmed red, then made green by the fix. Each asserts the failure message is the clean Content-Type ... is not defined diagnostic rather than could not be parsed as JSON:

  • ValidatesOpenApiSchemaTest::content_type_containing_json_substring_is_not_decoded_as_json (Laravel response)

  • ValidatesOpenApiSchemaAutoValidateRequestTest::content_type_containing_json_substring_is_not_decoded_as_json (Laravel request)

  • OpenApiAssertionsTest::content_type_containing_json_substring_is_not_decoded_as_json (Symfony)

  • composer test passes (1788 tests, 4201 assertions)

  • composer stan passes (no errors)

  • composer cs-check passes (no violations)

Notes for reviewers

  • ContentTypeMatcher is @internal; intra-package use from the adapters mirrors how RequestBodyValidator / ResponseBodyValidator already consume it.
  • Misleading docblock/inline comments on the three methods ("report an absent body so the validator decides whether the spec required one") were clarified — the validator decides via Content-Type negotiation on the separately-passed Content-Type, not via the body envelope.

wadakatu added 2 commits May 18, 2026 15:27
…TypeMatcher

The three adapter body-extraction methods (Laravel `extractRequestBody` /
`extractJsonBody`, Symfony `extractSymfonyJsonBody`) decided whether to
JSON-decode a body with a loose `str_contains(strtolower($contentType),
'json')` check. That treats borderline media types such as
`application/jsonsomethingweird` as JSON, so the adapter would attempt a
`json_decode()` on a non-JSON body and fail with a misleading "could not be
parsed as JSON" parse error.

Switch the three methods to `ContentTypeMatcher::isJsonContentType()` (fed
through `normalizeMediaType()`) — the same strict check the body validators
already use. The adapter and validator now agree on exactly which media
types count as JSON: a non-JSON body is left undecoded and reported absent,
and the validator's content negotiation surfaces its clean "Content-Type is
not defined" diagnostic instead.

Standard types (`application/json`, `application/*+json`,
`application/json; charset=utf-8`) are unaffected.

Closes #251
…ct extractRequestBody docblock

Review follow-ups for #251:

- ContentTypeMatcherTest: add an explicit negative case pinning that media
  types which merely contain the substring "json" (text/json,
  application/jsonp, application/jsonsomethingweird) are not JSON. This is
  the contract the adapters now delegate to, and text/json / application/jsonp
  are behavior-change cases relative to the old loose check.
- extractRequestBody() docblock: the previous wording promised a "loud"
  verdict unconditionally. The request-side validator only reports an
  undeclared media type when the operation declares a requestBody content
  map, and a declared non-JSON media type is accepted without body-schema
  validation (the schema engine is JSON-only). Reworded to match.
@wadakatu
Copy link
Copy Markdown
Collaborator Author

Multi-agent review (/pr-review-toolkit:review-pr) — addressed

Ran code-reviewer / pr-test-analyzer / comment-analyzer / silent-failure-hunter. No critical or important bugs in the code. Two review findings addressed in 0fd8888:

  • Docblock accuracyextractRequestBody()'s docblock claimed the validator always delivers a "loud" verdict. Reworded: the request-side validator only reports an undeclared media type when the operation declares a requestBody content map, and a declared non-JSON media type is accepted without body-schema validation (JSON-only engine).
  • Boundary coverage — added an explicit ContentTypeMatcher negative case pinning that text/json / application/jsonp / application/jsonsomethingweird (substring json but not JSON) are non-JSON. text/json & application/jsonp are behavior-change cases vs the old loose check.

Follow-up issue filed

silent-failure-hunter flagged a pre-existing gap (out of scope here): a non-JSON Content-Type declared in the spec with a schema is accepted without validating the body and without any skip diagnostic — and the response-side Skipped surfacing is bypassed when the type matches a spec key. Tracked in #254.

@wadakatu wadakatu merged commit 61a8299 into main May 18, 2026
16 checks passed
@wadakatu wadakatu deleted the fix/non-json-content-type-issue-251 branch May 18, 2026 08:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tech-debt(adapters) — 非空・非JSON Content-Type のボディが silent に「ボディ無し」扱いされる

1 participant