Skip to content

Distinguish request vs response source on JsonSchemaException (#369)#370

Merged
koriym merged 7 commits into
bearsunday:1.xfrom
koriym:feature/369-request-response-exception-source
May 28, 2026
Merged

Distinguish request vs response source on JsonSchemaException (#369)#370
koriym merged 7 commits into
bearsunday:1.xfrom
koriym:feature/369-request-response-exception-source

Conversation

@koriym
Copy link
Copy Markdown
Member

@koriym koriym commented May 28, 2026

Closes #369

Important

Stacks on top of #367 — please merge #367 first. Until #367 lands, the diff against 1.x will show #367's changes too. After #367 merges, this PR will rebase to only show #369.

cc @koriym

Summary

Two concrete subclasses of JsonSchemaException let direct-catch code discriminate client-supplied bad input from off-schema output:

  • JsonSchemaRequestException — request-parameter validation failed (Code::BAD_REQUEST, 400)
  • JsonSchemaResponseException — response-body validation failed (Code::ERROR, 500)
try {
    $resource->get('app://self/user', ['id' => 'not-an-int']);
} catch (JsonSchemaRequestException $e) {
    // bad client input → 4xx
} catch (JsonSchemaResponseException $e) {
    // off-schema output → 5xx + alert
}

Design notes

  • final dropped from the parent; nothing else about the parent changes. Existing catch (JsonSchemaException $e) still matches both subclasses — BC safe.
  • Interceptor validate() split into validateAsRequest() / validateAsResponse(). Each throws its specific subclass directly — no class-string indirection or if/else over the target type.
  • Shared logic in two helpers: runValidator() (justinrainbow invocation) + buildValidationFailure() returning array{string, JsonSchemaErrors} for the message + structured-error pair.
  • Catch sites narrowed to the concrete subclass — no assert($e instanceof ...) needed. Safe because JsonSchemaNotFoundException extends LogicException directly, not JsonSchemaException, so it bubbles past the catch.
  • Handler interface @param PHPDoc narrowed to the concrete subtype the interceptor actually delivers. Runtime parameter type stays on the parent for BC with existing implementations.

Design history

The class-string approach was tried first (codex's initial pass). Switched to the split-method shape after review — codex itself agreed on the second pass: "split is cleaner, narrow catch is sound, two-helper split is natural".

CI

Test plan

  • vendor/bin/phpunit
  • vendor/bin/phpcs --standard=phpcs.xml src tests
  • vendor/bin/phpstan --memory-limit=-1 analyse -c phpstan.neon
  • vendor/bin/psalm --monochrome --threads=1 --no-cache
  • vendor-bin/tools/vendor/bin/phpmd --exclude src/Annotation src text ./phpmd.xml
  • CI on PR

🤖 Generated with Claude Code

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

Review Change Stack

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8aa34f97-4b12-4a35-8e1b-ac1bdc955d7e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

JSON Schema検証例外の出力元を識別可能にするため、JsonSchemaException を継承可能にしてリクエスト (4xx) 検証失敗用 JsonSchemaRequestException とレスポンス (5xx) 検証失敗用 JsonSchemaResponseException サブクラスを追加。インターセプター・ハンドラーインターフェース・テスト・ドキュメントを一貫して更新。既存コードの後方互換性を保持。

Changes

JSON Schema 例外の階層化と実装

Layer / File(s) Summary
親例外クラスの修正と型定義
src/JsonSchema/Exception/JsonSchemaException.php, src/Types.php
JsonSchemaException から final を削除して継承可能にする。HTTP ステータスコード範囲を表す ClientErrorCode (400〜499) と ServerErrorCode (500〜599) の Psalm 型エイリアスを追加。
リクエスト/レスポンス固有例外サブクラスの定義
src/JsonSchema/Exception/JsonSchemaRequestException.php, src/JsonSchema/Exception/JsonSchemaResponseException.php
JsonSchemaRequestException (デフォルト Code::BAD_REQUEST) と JsonSchemaResponseException (デフォルト Code::ERROR) を新規追加。両クラスともコンストラクタで受け取った message・code・errors を親へ委譲。
インターセプターの検証ロジック再構成
src/JsonSchema/Interceptor/JsonSchemaInterceptor.php
import を新しいサブクラスに切り替え、validateAsRequest() (リクエスト検証、Code::BAD_REQUEST を投げる) と validateAsResponse() (レスポンス検証、Code::ERROR を投げる) を新規実装。runValidator()buildValidationFailure() で検証実行とエラー生成を分離。
ハンドラーインターフェースのドキュメント更新
src/JsonSchema/JsonSchemaExceptionHandlerInterface.php, src/JsonSchema/JsonSchemaRequestExceptionHandlerInterface.php
JsonSchemaResponseException / JsonSchemaRequestException が常に引き渡される旨を PHPDoc で明記。実行時型互換のため引数型は親クラスのまま維持。
新しい例外クラスのテスト実装
tests/JsonSchema/Exception/JsonSchemaRequestExceptionTest.php, tests/JsonSchema/Exception/JsonSchemaResponseExceptionTest.php
継承関係・メッセージ/コード保持・構造化エラー保持を検証する PHPUnit テストクラスを追加。
インテグレーションテストの追加
tests/Module/JsonSchemaModuleTest.php
request/response 検証の例外種別を確認するテストメソッドを追加。新しいサブクラス例外が正しく投げられ、かつ親クラスとしても catch 可能なことを検証。
ドキュメント更新
CHANGELOG.md, README.md, README.ja.md
新しいサブクラス追加、request/response の区別、getErrors() が構造化エラーの唯一の情報源であることを記載・説明。

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • bearsunday/BEAR.Resource#367: 本PR は #367 で導入された構造化エラー DTO (JsonSchemaErrors, JsonSchemaError, ConstraintViolation) を利用してさらに、JSON Schema検証例外を request/response 別に区別可能にするサブクラス化を実装。
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR title clearly describes the main objective: introducing source discrimination (request vs response) for JsonSchemaException.
Description check ✅ Passed PR description is relevant and comprehensive, explaining the purpose, design, and implementation details of the changes.
Linked Issues check ✅ Passed Code changes fully satisfy #369 requirements: two concrete subclasses introduced, parent loses final, instanceof discrimination enabled, BC preserved.
Out of Scope Changes check ✅ Passed All changes align with #369 scope: exception class hierarchy, interceptor refactoring, documentation updates, and tests for new subclasses.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (ed78c4c) to head (7877cb3).

Additional details and impacted files
@@             Coverage Diff             @@
##                 1.x      #370   +/-   ##
===========================================
  Coverage     100.00%   100.00%           
- Complexity       683       688    +5     
===========================================
  Files             94        96    +2     
  Lines           1707      1720   +13     
===========================================
+ Hits            1707      1720   +13     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@koriym
Copy link
Copy Markdown
Member Author

koriym commented May 28, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…nday#369)

Two concrete subclasses of JsonSchemaException let direct-catch code
discriminate between client-supplied bad input and a resource producing
off-schema output:

- JsonSchemaRequestException — request parameter validation failed
  (Code::BAD_REQUEST, 400)
- JsonSchemaResponseException — response body validation failed
  (Code::ERROR, 500)

`final` is dropped from the parent so subclassing is permitted. The
parent constructor and getErrors() signature stay unchanged, so
existing `catch (JsonSchemaException $e)` keeps working and structured
errors flow through subclasses automatically (bearsunday#367's JsonSchemaErrors
ride along via inheritance).

Interceptor refactor:
- validate() split into validateAsRequest() / validateAsResponse(),
  each throws its specific subclass directly — no class-string dance.
- Shared logic split into runValidator() (justinrainbow invocation) and
  buildValidationFailure() (message + structured errors from a failed
  validator).
- catch sites narrowed to the concrete subclass so no
  `assert($e instanceof ...)` is needed. Safe because
  JsonSchemaNotFoundException extends LogicException directly, not
  JsonSchemaException, so it bubbles past the catch.

Handler interfaces (`JsonSchemaRequestExceptionHandlerInterface`,
`JsonSchemaExceptionHandlerInterface`) get a PHPDoc `@param` narrowed
to the concrete subtype the interceptor actually delivers. Runtime
parameter types stay on the parent for BC.

Closes bearsunday#369.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@koriym koriym force-pushed the feature/369-request-response-exception-source branch from 96a9cb7 to 9bf68a9 Compare May 28, 2026 02:05
koriym and others added 5 commits May 28, 2026 13:01
BEAR.Resource is a resource framework, not an HTTP framework. Pinning
`Code::BAD_REQUEST` / `Code::ERROR` onto the exception subclasses in the
docs read as if the class itself enforced the code — it doesn't. The
interceptor chooses those codes when throwing; subclasses inherit the
parent constructor unchanged and accept any code.

Same separation of concerns the interceptor itself observes: the
subclass carries the *semantic* (request vs response source), the
caller carries the *runtime value* (which numeric code to use).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds two `@psalm-type` aliases in `BEAR\Resource\Types`:

    @psalm-type ClientErrorCode = int<400, 499>
    @psalm-type ServerErrorCode = int<500, 599>

Each subclass overrides the parent constructor to narrow `@param int $code`
to the appropriate range:

- JsonSchemaRequestException::__construct  @param ClientErrorCode $code
  (defaults to Code::BAD_REQUEST)
- JsonSchemaResponseException::__construct  @param ServerErrorCode $code
  (defaults to Code::ERROR)

The point: BEAR.Resource is a resource framework, not an HTTP framework.
Whether a schema-validation request failure should map to 400, 422, or
some other 4xx isn't something the exception class can authoritatively
decide. Pinning a single value would lock the choice; using a wide
`int` would give callers no guidance. A range type lets the caller pick
the right code per their context while the type system blocks
nonsensical values (200, 999) statically.

Runtime behaviour: defaults unchanged. Interceptor still throws with
`Code::BAD_REQUEST` / `Code::ERROR`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The previous entries read like a design diary — names of internal types,
narrowing rationale, alternative approaches considered. None of that
helps a downstream consumer decide whether to upgrade or how. Two lines
each: what's new, where to read more.

The richer rationale already lives in the PR descriptions (bearsunday#364, bearsunday#369)
and the README sections.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace three paragraphs of justification with one code block. The
comments in the catch arms carry the same information (which validation
failed + the type-constrained `$code` range) without the prose
overhead. The BC fact ("parent catch matches both") stays as a one-liner.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
「リクエスト・レスポンス由来の区別」suggests "discrimination of
request-response origin" which is unnatural — `由来` qualifies events,
not the act of discriminating. Use a slash separator instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@koriym koriym marked this pull request as ready for review May 28, 2026 04:46
@koriym
Copy link
Copy Markdown
Member Author

koriym commented May 28, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.ja.md`:
- Around line 468-469: Replace the PHPDoc-style inline ranges in the catch
comments for JsonSchemaRequestException and JsonSchemaResponseException with
plain-language descriptions; for example change the comment on
JsonSchemaRequestException to "リクエスト検証失敗(HTTPステータス 400–499)" and the comment on
JsonSchemaResponseException to "レスポンス検証失敗(HTTPステータス 500–599)" so the intent is
clear without using `@param` int<...> syntax.

In `@README.md`:
- Around line 677-678: Replace the PHPDoc-style inline comments after the catch
blocks for JsonSchemaRequestException and JsonSchemaResponseException with
plain-language status code ranges; specifically change the comment for
JsonSchemaRequestException to something like "request validation failed — status
codes 400–499" and for JsonSchemaResponseException to "response validation
failed — status codes 500–599" so the intent is clear without using `@param`
PHPDoc syntax.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: fad71f08-dd1f-4ede-9630-f6c679a3a820

📥 Commits

Reviewing files that changed from the base of the PR and between ed78c4c and ba38ef1.

📒 Files selected for processing (13)
  • CHANGELOG.md
  • README.ja.md
  • README.md
  • src/JsonSchema/Exception/JsonSchemaException.php
  • src/JsonSchema/Exception/JsonSchemaRequestException.php
  • src/JsonSchema/Exception/JsonSchemaResponseException.php
  • src/JsonSchema/Interceptor/JsonSchemaInterceptor.php
  • src/JsonSchema/JsonSchemaExceptionHandlerInterface.php
  • src/JsonSchema/JsonSchemaRequestExceptionHandlerInterface.php
  • src/Types.php
  • tests/JsonSchema/Exception/JsonSchemaRequestExceptionTest.php
  • tests/JsonSchema/Exception/JsonSchemaResponseExceptionTest.php
  • tests/Module/JsonSchemaModuleTest.php

Comment thread README.ja.md Outdated
Comment thread README.md Outdated
`runValidator` was named that only because the original `validate()`
slot was taken. After splitting into `validateAsRequest()` /
`validateAsResponse()`, the slot is free and `validate()` is the
natural name for "run the validator and hand back its state".

Also fold in CodeRabbit's README nit: replace the inline `@param int<400,
499>` / `@param int<500, 599>` (PHPDoc syntax leaking into a code
comment) with plain `(4xx)` / `(5xx)`. The precise range types stay in
their canonical place (`src/Types.php` + the subclass constructors).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@koriym
Copy link
Copy Markdown
Member Author

koriym commented May 28, 2026

@coderabbitai approve

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

✅ Actions performed

Comments resolved and changes approved.

@koriym koriym merged commit be624b6 into bearsunday:1.x May 28, 2026
18 checks passed
@koriym koriym deleted the feature/369-request-response-exception-source branch May 28, 2026 05:37
koriym added a commit that referenced this pull request May 28, 2026
Reflect the additions from #367 (structured errors via JsonSchemaErrors
collection, JsonSchemaError DTO, render/format placeholder mechanism)
and #370 (JsonSchemaRequestException / JsonSchemaResponseException
subclasses with ClientErrorCode / ServerErrorCode range types).

- llms.txt: extend the JsonSchemaModule entry to surface the exception
  subclasses and the structured-errors collection so LLM consumers
  notice the API without reading source.
- llms-full.txt: same one-liner (preserves the strict-superset rule),
  plus two new subsections in JSON Schema Validation covering the
  request/response discrimination, range-typed @param, structured
  error access pattern, and the schema-side errorMessage lookup. Add
  two notes to the stale-assumptions list.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.

Distinguish request vs response source on JsonSchemaException (4xx vs 5xx)

1 participant