fix(adapters): type-check literal JSON null / scalar request & response bodies#247
Merged
Conversation
…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.
This was referenced May 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
リクエスト / レスポンスボディが JSON リテラルの
null(あるいは JSON スカラー)だった場合、json_decodeの結果が PHP のnullとなり、ボディ validator が「ボディ無し」と区別できませんでした。本 PR は内部マーカーPresentJsonNullを導入し、非空の生ボディがnullにデコードされたケースをアダプタがマーカーで包むことで、validator がスキーマと型照合するようにします。Why
Content-Type 無し + ボディが literal
null(4バイト)等の不正な内容のとき、json_decode('null')→nullが「ボディ無し」と同一視されていました。その結果:nullボディが型照合されず silent pass(contract テストの「不正は loud に落とす」原則に反する)nullが「Response body is empty」という不正確なメッセージで失敗extractJsonBodyの戻り値型?arrayでTypeErrorクラッシュ(Symfony アダプタはmixedのため正常)Closes #246
Verification
実装前に失敗テストを先に書き(TDD: RED 8件)、実装後 GREEN を確認しました。デグレ防止として、素の
null(空ボディ)が従来通り absent 扱いされること、スカラーボディが型照合されることも回帰テストで固定しています。composer testpasses(1768 tests)composer stanpasses(PHPStan level 6)composer cs-checkpassesNotes for reviewers
null=「ボディ無し」セマンティクスは不変。マーカーを渡すのは更新済みアダプタのみで、OpenApiResponseValidator::validate()/OpenApiRequestValidator::validate()の公開シグネチャ・引数も変更なし → SemVer 凍結 API に非抵触。直接呼び出し側の挙動は変わりません。extractJsonBody(Laravel)をTestResponse::json()から直接json_decode()へ変更しました。Laravel の「nulldecode = Invalid JSON」ヒューリスティックを回避し、Symfony アダプタと完全に挙動を揃えるためです(両アダプタともjson_decode(..., JSON_THROW_ON_ERROR)に統一)。private メソッドのため外部影響はありません。type: ["object", "null"]に対しては literalnullボディが正しく通過するようになります(修正前は「body is empty」で誤って reject)。これは意図した挙動改善です。フォローアップ Issue
multi-agent レビュー(
/pr-review-toolkit:review-pr)で挙がった、本 PR のスコープ外として切り出した項目:mixedを absent/present を表現する envelope 型に置き換える(type-design 指摘。公開シグネチャ変更のため v2 メジャーバンプ案件)OpenApiResponseValidatorのPresentJsonNullunwrap 経路の回帰テスト追加(pr-test 指摘)