Skip to content

fix(adapters): type-check literal JSON null / scalar request & response bodies#247

Merged
wadakatu merged 2 commits into
mainfrom
fix/literal-null-body-issue-246
May 18, 2026
Merged

fix(adapters): type-check literal JSON null / scalar request & response bodies#247
wadakatu merged 2 commits into
mainfrom
fix/literal-null-body-issue-246

Conversation

@wadakatu
Copy link
Copy Markdown
Collaborator

@wadakatu wadakatu commented May 18, 2026

Summary

リクエスト / レスポンスボディが JSON リテラルの null(あるいは JSON スカラー)だった場合、json_decode の結果が PHP の null となり、ボディ validator が「ボディ無し」と区別できませんでした。本 PR は内部マーカー PresentJsonNull を導入し、非空の生ボディが null にデコードされたケースをアダプタがマーカーで包むことで、validator がスキーマと型照合するようにします。

Why

Content-Type 無し + ボディが literal null(4バイト)等の不正な内容のとき、json_decode('null')null が「ボディ無し」と同一視されていました。その結果:

  • リクエスト側: ボディが optional のエンドポイントで、不正な null ボディが型照合されず silent pass(contract テストの「不正は loud に落とす」原則に反する)
  • レスポンス側: literal null が「Response body is empty」という不正確なメッセージで失敗
  • Laravel アダプタ: スカラーレスポンスボディが extractJsonBody の戻り値型 ?arrayTypeError クラッシュ(Symfony アダプタは mixed のため正常)

Closes #246

Verification

実装前に失敗テストを先に書き(TDD: RED 8件)、実装後 GREEN を確認しました。デグレ防止として、素の null(空ボディ)が従来通り absent 扱いされること、スカラーボディが型照合されることも回帰テストで固定しています。

  • composer test passes(1768 tests)
  • composer stan passes(PHPStan level 6)
  • composer cs-check passes

Notes for reviewers

  • 後方互換性: validator の null =「ボディ無し」セマンティクスは不変。マーカーを渡すのは更新済みアダプタのみで、OpenApiResponseValidator::validate() / OpenApiRequestValidator::validate() の公開シグネチャ・引数も変更なし → SemVer 凍結 API に非抵触。直接呼び出し側の挙動は変わりません。
  • extractJsonBody(Laravel)を TestResponse::json() から直接 json_decode() へ変更しました。Laravel の「null decode = Invalid JSON」ヒューリスティックを回避し、Symfony アダプタと完全に挙動を揃えるためです(両アダプタとも json_decode(..., JSON_THROW_ON_ERROR) に統一)。private メソッドのため外部影響はありません。
  • OAS 3.1 の type: ["object", "null"] に対しては literal null ボディが正しく通過するようになります(修正前は「body is empty」で誤って reject)。これは意図した挙動改善です。

フォローアップ Issue

multi-agent レビュー(/pr-review-toolkit:review-pr)で挙がった、本 PR のスコープ外として切り出した項目:

wadakatu added 2 commits May 18, 2026 13:50
…se bodies

A request/response body of the literal JSON `null` (or a JSON scalar)
decoded to PHP `null`, which the body validators could not distinguish
from an absent body. This let a malformed body slip through silently
(optional request body) or fail with a misleading "body is empty"
message.

Introduce an internal `PresentJsonNull` marker enum: the Laravel and
Symfony adapters wrap a decoded `null` from non-empty raw content in
this marker so `ResponseBodyValidator` / `RequestBodyValidator`
type-check it against the schema instead of short-circuiting as "no
body".

The validators' `null` = "no body" semantics and the public
`validate()` signatures are unchanged, so direct callers are
unaffected (backward compatible).

`extractJsonBody` (Laravel) now decodes via `json_decode()` instead of
`TestResponse::json()` and returns `mixed` instead of `?array`, fixing
a `TypeError` on scalar response bodies and aligning behaviour with
the Symfony adapter.

Add regression tests across both body validators and the Laravel /
Symfony adapters.

Closes #246
Addresses review feedback on PR #247 (issue #246 fix).

Move the decoded-body `return` inside the `try` block in all three body
extractors (`extractRequestBody`, `extractJsonBody`,
`extractSymfonyJsonBody`) so the return's dependence on a successful
decode is local and explicit, rather than relying on
`failOpenApi(): never` to keep `$decoded` defined after the `catch`.

Correct the `extractRequestBody` / `extractSymfonyJsonBody` docblocks:
JSON is parsed when the Content-Type claims JSON OR is absent, not only
when it claims JSON.

Document on the `PresentJsonNull` enum that every consumer must unwrap
the marker before passing the value to schema conversion or the
strict-required walker.

Add regression tests for OAS 3.0 `nullable: true` (literal-null body
accepted, request + response) and for the literal-null fix on the
explicit `Content-Type: application/json` path.

No behavior change; full suite (1768 tests), PHPStan and PHP-CS-Fixer
all pass.
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 body decoding to literal null / scalar is silently treated as "no body"

1 participant