Skip to content

Commit f4d64fc

Browse files
authored
feat: Surface oneOf/anyOf validation failures in an RFC 9457 errors[] array (#104)
1 parent 5648091 commit f4d64fc

14 files changed

Lines changed: 1509 additions & 72 deletions

README.md

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
A lightweight Java library that wraps the JDK's `com.sun.net.httpserver.HttpServer` and serves
1111
endpoints declared in an OpenAPI 3.1.x specification. Handlers are pure functions registered by
1212
`operationId`; the framework handles routing, OpenAPI parameter and body validation, JSON
13-
(de)serialisation, and RFC 7807 error rendering.
13+
(de)serialisation, and RFC 9457 error rendering.
1414

1515
## Table of contents
1616

@@ -27,7 +27,7 @@ endpoints declared in an OpenAPI 3.1.x specification. Handlers are pure function
2727
- [After-response hooks](#after-response-hooks)
2828
- [Security](#security)
2929
- [Request body content types](#request-body-content-types)
30-
- [Error responses (RFC 7807)](#error-responses-rfc-7807)
30+
- [Error responses (RFC 9457)](#error-responses-rfc-9457)
3131
- [Extra (non-OpenAPI) handlers](#extra-non-openapi-handlers)
3232
- [Health endpoint](#health-endpoint)
3333
- [Graceful shutdown](#graceful-shutdown)
@@ -47,7 +47,7 @@ endpoints declared in an OpenAPI 3.1.x specification. Handlers are pure function
4747
`ResponseDecorator` for cross-cutting response headers
4848
- OpenAPI `securitySchemes` and `security` enforcement (`apiKey`, `http bearer`, `http basic`),
4949
with an opt-out for sidecar / gateway authentication
50-
- RFC 7807 `application/problem+json` validation errors with JSON-Pointer to the failing location
50+
- RFC 9457 `application/problem+json` validation errors with an `errors[]` array of JSON-Pointers to the failing locations
5151
- Built on the JDK's native `HttpServer` with thread-per-request behaviour using virtual threads
5252

5353
## Maven artifact
@@ -630,7 +630,7 @@ to detect errors.
630630

631631
The library parses `components.securitySchemes` and the `security` requirement lists (root-level
632632
and per-operation), extracts the credential per scheme, hands it to a consumer-provided
633-
`SchemeValidator` callback, and renders RFC 7807 `application/problem+json` rejections — 401 for
633+
`SchemeValidator` callback, and renders RFC 9457 `application/problem+json` rejections — 401 for
634634
missing/malformed credentials (with `WWW-Authenticate`), 403 when the validator denies.
635635

636636
Supported scheme types in this release:
@@ -870,28 +870,38 @@ case-insensitive):
870870

871871
Form-field coercion mirrors the rules already used at the parameter boundary: the wire is
872872
string-only by definition, so a property typed as `integer` accepts `"42"` and yields `42`.
873-
Coercion failures surface as RFC-7807 `400` responses with a JSON-pointer to the failing field.
873+
Coercion failures surface as RFC-9457 `400` responses with a JSON-pointer to the failing field.
874874

875875
Both built-in parsers honour the `charset=` parameter on the `Content-Type` header (default
876876
UTF-8). Unknown charsets fall back to UTF-8.
877877

878-
## Error responses (RFC 7807)
878+
## Error responses (RFC 9457)
879879

880880
Validation failures — missing required fields, type mismatches, unsupported content types,
881881
coercion errors, malformed bodies — produce an `HTTP 400 Bad Request` response with body media
882882
type `application/problem+json`, following
883-
[RFC 7807](https://datatracker.ietf.org/doc/html/rfc7807).
883+
[RFC 9457](https://datatracker.ietf.org/doc/html/rfc9457) (which obsoletes RFC 7807).
884884

885-
A single error is reported per request (first failure wins). The response body has these fields:
885+
The top level carries the RFC core members; each individual failure is an entry in an `errors`
886+
array (an RFC 9457 extension member). A non-combinator failure yields a single entry; a
887+
`oneOf` / `anyOf` failure yields one entry per failed branch, ordered most-likely-cause first
888+
(the branch the payload most resembles) and de-duplicated.
886889

887890
| Field | Type | Description |
888891
| ---------- | ------- | ---------------------------------------------------------------------------------------- |
889892
| `type` | string | Always `about:blank` (no per-error type URI). |
890893
| `title` | string | Always `Bad Request`. |
891894
| `status` | integer | Always `400`. |
892-
| `detail` | string | Human-readable description of the failure (e.g. `expected integer`). |
893-
| `pointer` | string | [RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901) JSON-Pointer to the failing location (e.g. `/body/age`, `/query/limit`, `/path/id`, or `/body` for body-wide errors). |
895+
| `detail` | string | Human-readable description (a leaf message; for a combinator, `matched 0 of N oneOf branches` or `did not match any anyOf branch`). |
896+
| `errors` | array | One entry per failure; omitted when empty. Each entry has the fields below. |
897+
898+
Each `errors[]` entry:
899+
900+
| Field | Type | Description |
901+
| ---------- | ------- | ---------------------------------------------------------------------------------------- |
902+
| `pointer` | string | [RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901) JSON-Pointer to the failing location, as a URI fragment — e.g. `#/age` for a body field, `#/query/limit` / `#/path/id` for parameters, `#/body` for whole-body errors (missing body, unsupported content type), or `#` when the entire body is the wrong type. |
894903
| `keyword` | string | The validation rule that failed: `type`, `required`, `enum`, `pattern`, `format`, `minimum`, `maximum`, `minLength`, `maxLength`, `additionalProperties`, `oneOf`, `anyOf`, `allOf`, `not`, `const`, `content-type`, `decode`, … |
904+
| `detail` | string | Human-readable description of this failure (e.g. `expected integer`). |
895905

896906
Example body for `POST /form-echo` with `age=abc` (`age` is declared as `integer`):
897907

@@ -901,11 +911,31 @@ Example body for `POST /form-echo` with `age=abc` (`age` is declared as `integer
901911
"title": "Bad Request",
902912
"status": 400,
903913
"detail": "expected integer",
904-
"pointer": "/age",
905-
"keyword": "type"
914+
"errors": [
915+
{ "pointer": "#/age", "keyword": "type", "detail": "expected integer" }
916+
]
917+
}
918+
```
919+
920+
Example body for a `oneOf` request body that matches no branch — one entry per failed branch,
921+
deepest (most-likely) first:
922+
923+
``` json
924+
{
925+
"type": "about:blank",
926+
"title": "Bad Request",
927+
"status": 400,
928+
"detail": "matched 0 of 2 oneOf branches",
929+
"errors": [
930+
{ "pointer": "#/pet/collar/size", "keyword": "type", "detail": "expected integer" },
931+
{ "pointer": "#/pet/bark", "keyword": "type", "detail": "expected boolean" }
932+
]
906933
}
907934
```
908935

936+
When several branches fail at the same location for the same reason, those identical entries are
937+
collapsed into one — so a `oneOf` failure can show fewer entries than it has branches.
938+
909939
Other error responses:
910940

911941
- **404 Not Found** — no route matches the request path (no body).

0 commit comments

Comments
 (0)