Skip to content

Commit 2f66507

Browse files
authored
feat(core): validate ErrorSource.pointer as RFC 6901
1 parent 15f3103 commit 2f66507

10 files changed

Lines changed: 262 additions & 23 deletions

File tree

‎.agentWork/milestones/README.md‎

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,35 +23,37 @@ Milestones are planned, testable increments. They may change until implementatio
2323
feeds later PATCH binding.
2424
15. **Phase 1.4 — core identity/linkage hardening:** follows Phase 1.1/1.3; closes alias-aware
2525
identifier-collection uniqueness and related core regressions without Phase 4.1 scope.
26-
16. **Phase 2.1 — Jackson 3 document writer:** creates the first major-specific codec artifact and
26+
16. **Phase 1.5 — error source pointer conformance:** follows Phase 1.1/1.4; RFC 6901 syntax for
27+
`ErrorSource.pointer` without document resolution or Phase 4.1 scope.
28+
17. **Phase 2.1 — Jackson 3 document writer:** creates the first major-specific codec artifact and
2729
proves deterministic model-to-wire behavior.
28-
17. **Phase 2.2 — Jackson 3 write mapping**, **Phase 2.4 — document reader**, and **Phase 2.5 —
30+
18. **Phase 2.2 — Jackson 3 write mapping**, **Phase 2.4 — document reader**, and **Phase 2.5 —
2931
draft-schema cross-check:** completed; **Phase 2.3 — compound serialization** and **Phase 2.9 —
3032
flat DTO reader:** may proceed in parallel after their listed dependencies.
31-
18. **Phase 2.3 — Jackson 3 compound serialization** and **Phase 2.9 — flat DTO reader:** add
33+
19. **Phase 2.3 — Jackson 3 compound serialization** and **Phase 2.9 — flat DTO reader:** add
3234
explicit inclusion and validated resource-to-DTO binding independently.
33-
19. **Phase 2.8 — Jackson 3 sparse fieldsets** and **Phase 2.10 — typed domain envelope:** build on
35+
20. **Phase 2.8 — Jackson 3 sparse fieldsets** and **Phase 2.10 — typed domain envelope:** build on
3436
their respective compound and flat-read foundations.
35-
20. **Phase 2.11 — Jackson 3 PATCH binding:** composes update validation and typed DTO envelopes
37+
21. **Phase 2.11 — Jackson 3 PATCH binding:** composes update validation and typed DTO envelopes
3638
into presence-aware commands.
37-
21. **Phase 3.1 — query parser:** remains an independent optional artifact.
38-
22. **Phase 3.2 — Spring WebMVC document transport:** integrates media negotiation, validated
39+
22. **Phase 3.1 — query parser:** remains an independent optional artifact.
40+
23. **Phase 3.2 — Spring WebMVC document transport:** integrates media negotiation, validated
3941
documents, query arguments, and safe errors.
40-
23. **Phase 3.3 — Spring WebMVC flat DTO binding:** adds the primary Jackson 3/Spring DTO,
42+
24. **Phase 3.3 — Spring WebMVC flat DTO binding:** adds the primary Jackson 3/Spring DTO,
4143
envelope, inclusion/fieldset, and PATCH experience.
42-
24. **Phase 3.4 — WebFlux evaluation:** begins after document and DTO-oriented WebMVC behavior is
44+
25. **Phase 3.4 — WebFlux evaluation:** begins after document and DTO-oriented WebMVC behavior is
4345
stable.
44-
25. **Phase 2.6 — Jackson 2 document writer:** starts the later parity track after the Jackson
46+
26. **Phase 2.6 — Jackson 2 document writer:** starts the later parity track after the Jackson
4547
3/Spring path, without adding an artificial Spring dependency.
46-
26. **Phase 2.7 — Jackson 2 document reader** and **Phase 2.12 — domain mapping:** may proceed after
48+
27. **Phase 2.7 — Jackson 2 document reader** and **Phase 2.12 — domain mapping:** may proceed after
4749
the Jackson 2 writer and their respective Jackson 3 contracts.
48-
27. **Phase 2.13 — Jackson 2 compound serialization** and **Phase 2.15 — flat DTO reader:** build
50+
28. **Phase 2.13 — Jackson 2 compound serialization** and **Phase 2.15 — flat DTO reader:** build
4951
independently on stable mapping/read contracts.
50-
28. **Phase 2.14 — Jackson 2 sparse fieldsets** and **Phase 2.16 — typed domain envelope:** finish
52+
29. **Phase 2.14 — Jackson 2 sparse fieldsets** and **Phase 2.16 — typed domain envelope:** finish
5153
write-policy and read-envelope parity independently.
52-
29. **Phase 2.17 — Jackson 2 PATCH binding:** completes presence-aware DTO parity.
53-
30. **Phase 4.1 — conformance and hardening.**
54-
31. **Phase 4.2 — stable release.**
54+
30. **Phase 2.17 — Jackson 2 PATCH binding:** completes presence-aware DTO parity.
55+
31. **Phase 4.1 — conformance and hardening.**
56+
32. **Phase 4.2 — stable release.**
5557

5658
## Milestone index
5759

@@ -74,6 +76,7 @@ milestone file.
7476
- [Phase 1.2 — Domain-Mapping Annotations](phase-1-2-annotations.md) — `jsonapi-java-annotations` — Complete
7577
- [Phase 1.3 — Resource Update Request Validation](phase-1-3-update-request-validation.md) — `jsonapi-java-core` — Complete
7678
- [Phase 1.4 — Core Identity and Linkage Hardening](phase-1-4-core-identity-linkage-hardening.md) — `jsonapi-java-core` — Complete
79+
- [Phase 1.5 — Error Source Pointer Conformance](phase-1-5-error-source-pointer-conformance.md) — `jsonapi-java-core` — Complete
7780
- [Phase 2.1 — Jackson 3 Document Writer](phase-2-1-jackson-document-codec.md) — `jsonapi-java-jackson3` — Complete
7881
- [Phase 2.2 — Jackson 3 Domain-to-Resource Mapping](phase-2-2-domain-resource-mapping.md) — `jsonapi-java-jackson3` — Complete
7982
- [Phase 2.3 — Jackson 3 Compound Serialization Context](phase-2-3-compound-serialization.md) — `jsonapi-java-jackson3` — Complete
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Phase 1.5 — Error Source Pointer Conformance
2+
3+
> **Module:** `jsonapi-java-core`
4+
> **Packages:** `io.github.kazemek.jsonapi.core.model`, `io.github.kazemek.jsonapi.core.internal`,
5+
> `io.github.kazemek.jsonapi.core.validation`
6+
> **Dependencies:** Phase 1.1, Phase 1.4
7+
> **Status:** Complete
8+
9+
## Goal
10+
11+
Make `ErrorSource.pointer` reject non–RFC 6901 JSON Pointer syntax while keeping the existing
12+
public API and architecture intact.
13+
14+
## Research and constraints
15+
16+
- [JSON:API 1.1 error objects](https://jsonapi.org/format/1.1/#error-objects) — `errors[].source.pointer`
17+
is a JSON Pointer identifying a value in the request document. Consequence: validate pointer
18+
syntax; do not resolve against a document in core.
19+
- [RFC 6901](https://www.rfc-editor.org/rfc/rfc6901) — empty string `""` is a valid pointer; a
20+
non-empty pointer must start with `/`; only `~0` and `~1` are valid escapes inside a reference
21+
token (`~01` is valid; bare `~`, `~2`, and other `~` forms are invalid); reference tokens may
22+
contain Unicode. Do not accept URI-fragment form (`#/…`) for `ErrorSource.pointer`.
23+
- [ADR-003](../../docs/adr/003-validation-and-immutability.md) — local invariants belong in the
24+
public construction path via stable `ValidationRuleCode` and JSON Pointer-like paths; keep
25+
local vs aggregate separation; do not extract a new validation service for this change.
26+
- Existing pattern: [`Link`](../../jsonapi-java-core/src/main/java/io/github/kazemek/jsonapi/core/model/Link.java)
27+
uses [`SyntaxValidators`](../../jsonapi-java-core/src/main/java/io/github/kazemek/jsonapi/core/internal/SyntaxValidators.java)
28+
+ `LocalValidation.fail` at compact-constructor time. [`ErrorSource`](../../jsonapi-java-core/src/main/java/io/github/kazemek/jsonapi/core/model/ErrorSource.java)
29+
currently validates only `additionalMembers` under base path `/errors/source` and accepts arbitrary
30+
`pointer` strings — this is a stricter-validation behavior change for previously accepted invalid
31+
pointers.
32+
- [`docs/conformance.md`](../../docs/conformance.md) has “Error source additional members” but no
33+
`source.pointer` / RFC 6901 row yet; add one (syntax-only, no resolution).
34+
- Existing [`JsonPointers`](../../jsonapi-java-core/src/main/java/io/github/kazemek/jsonapi/core/internal/JsonPointers.java)
35+
is emit/escape/build only — do **not** extend it for inbound syntax validation; put the check in
36+
`SyntaxValidators` like other inbound syntax helpers.
37+
- No new ADR. Nullness unchanged (`pointer` already `@Nullable`). Public validate-flow / diagnostic
38+
surface changes (`ErrorSource` construction + new `ValidationRuleCode`), so refresh
39+
`jsonapi-java-core` docs per the `module-docs` skill (do not copy its checklist into this file).
40+
41+
## Deliverables
42+
43+
1. **JSON Pointer syntax helper** in `SyntaxValidators` (e.g. `isValidJsonPointer(String)`),
44+
syntax-only, matching existing internal conventions; no new package or public type; do not
45+
extend `JsonPointers` for validation.
46+
2. **`ErrorSource` constructor validation:** when `pointer` is non-null, accept valid RFC 6901
47+
syntax and reject invalid syntax with `ValidationRuleCode.INVALID_JSON_POINTER` at
48+
`/errors/source/pointer`; `null` remains valid (optional member).
49+
3. **Focused Spock coverage:** extend `SyntaxValidatorsSpec` for the RFC 6901 boolean matrix, and
50+
model/`ErrorSource` specs for `INVALID_JSON_POINTER` + `/errors/source/pointer` (including `""`,
51+
escapes, `~01`, bare `~`, missing leading `/`, Unicode, and rejected URI-fragment `#/…`).
52+
4. **Conformance row** in `docs/conformance.md` marking `source.pointer` RFC 6901 syntax as
53+
supported, syntax-only, with no document resolution in core.
54+
5. **Module docs** for `jsonapi-java-core` per the `module-docs` skill: agent/invariant and
55+
entry-point notes for syntax-only `ErrorSource.pointer` RFC 6901 validation, linking
56+
conformance.
57+
58+
## Non-goals
59+
60+
- Resolving pointers against request/response documents or checking that a pointer targets a
61+
valid JSON:API member.
62+
- URI-fragment JSON Pointer syntax (`#/…`).
63+
- Semantic validation of `parameter`, `header`, or broader `ErrorObject` rules.
64+
- Public `JsonPointer` type, new packages, new validation services, extracting `SyntaxValidators`,
65+
extending `JsonPointers` for inbound validation, new dependencies, or a new ADR.
66+
- Phase 4.1 conformance/hardening umbrella work; Jackson/Spring modules.
67+
68+
## Implementation boundaries
69+
70+
- Touch `jsonapi-java-core` only: `SyntaxValidators`, `ErrorSource`, `ValidationRuleCode`, mirrored
71+
Spock specs (`SyntaxValidatorsSpec` and model/`ErrorSource`), `docs/conformance.md`, and
72+
`jsonapi-java-core` module docs required by `module-docs`.
73+
- Do not change `ErrorSource`’s public record shape, `ErrorObject`, or `JsonApiDocumentValidator`
74+
unless the existing local-validation architecture absolutely requires it (prefer constructor-only).
75+
- Public API surface unchanged except stricter rejection of invalid pointer strings and one new
76+
public enum constant on `ValidationRuleCode`.
77+
78+
## Test strategy
79+
80+
- Helper (`SyntaxValidatorsSpec`): boolean table for `""`, `/`, `/data`, `/data/0/id`, `/a~0b`,
81+
`/a~1b`, `/a~01b`, Unicode token path (e.g. `/données`) → true; `data`, `/a~`, `/a~2`, `/a~x`,
82+
`#/data` → false.
83+
- Model/`ErrorSource`: `pointer == null` and valid pointers construct successfully; invalid
84+
pointers throw `JsonApiValidationException` with `ruleCode == INVALID_JSON_POINTER` and path
85+
`/errors/source/pointer` (mirror `LinkSpec` conventions).
86+
87+
## Acceptance criteria
88+
89+
- [x] Non-null `ErrorSource.pointer` accepts RFC 6901 syntax (including `""`, `~0`/`~1`/`~01`, and
90+
Unicode tokens) and rejects invalid syntax with `INVALID_JSON_POINTER` at
91+
`/errors/source/pointer`.
92+
- [x] `pointer == null` remains valid; no public API redesign, no document-resolution logic, and no
93+
new dependency.
94+
- [x] `docs/conformance.md` documents syntax-only RFC 6901 support for `source.pointer`.
95+
- [x] The canonical `module-docs` checklist passes for `jsonapi-java-core`.
96+
- [x] `./gradlew :jsonapi-java-core:test --tests 'io.github.kazemek.jsonapi.core.internal.SyntaxValidatorsSpec' --tests 'io.github.kazemek.jsonapi.core.model.*'`
97+
passes.
98+
- [x] `./gradlew clean build` passes.
99+
- [x] Spotless passes (`./gradlew spotlessApply` then `./gradlew spotlessCheck`).
100+
- [x] When `SONAR_TOKEN` is available, the Sonar Quality Gate passes; without it, report Sonar
101+
blocked and that CI must still pass the gate.

‎docs/conformance.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
Conformance is reported per feature as: **supported**, **pass-through**, **delegated**, **deferred**, or **out of scope**.
44

55
This checklist is seeded by Phase 1.1 (`jsonapi-java-core`), Phase 1.2
6-
(`jsonapi-java-annotations`), Phase 1.3 (`jsonapi-java-core` update validation), Phase 2.1
6+
(`jsonapi-java-annotations`), Phase 1.3 (`jsonapi-java-core` update validation), Phase 1.5
7+
(`jsonapi-java-core` error `source.pointer` RFC 6901 syntax), Phase 2.1
78
(`jsonapi-java-jackson3` document writer), Phase 2.2 (`jsonapi-java-jackson3` domain-to-resource
89
write mapping), Phase 2.4 (`jsonapi-java-jackson3` document reader), and cross-checked by
910
Phase 2.5 against pinned JSON:API 1.1 draft schemas. Flat resource-to-DTO binding (Phase 2.9)
@@ -33,6 +34,7 @@ included binding remain **deferred** to their Phase 2 milestones.
3334
| Nullable pagination links | supported | `Links` null-preserving map |
3435
| Meta flat object (no synthetic `members` key) | supported | `Meta` |
3536
| Error object requires ≥1 standard member | supported | `ErrorObject` |
37+
| Error `source.pointer` RFC 6901 syntax | supported | `ErrorSource.pointer`; syntax only via `SyntaxValidators.isValidJsonPointer`; empty string allowed; no document resolution; URI-fragment form rejected |
3638
| Error source additional members | pass-through | `ErrorSource.additionalMembers` |
3739
| Additional member name grammar | supported | `MemberNames` (alphanumeric namespaces, may start with digit) |
3840
| Reserved dedicated members in additional maps | supported | Document, resource, identifier, relationship, error, jsonapi, link, source |

‎jsonapi-java-core/README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ This module does not provide Jackson codecs, HTTP adapters, query-parameter pars
3737

3838
## For contributors / agents
3939

40-
- **Local vs aggregate:** Compact constructors enforce single-value invariants. Cross-document rules live only in `JsonApiDocumentValidator`.
40+
- **Local vs aggregate:** Compact constructors enforce single-value invariants (including RFC 6901
41+
syntax for `ErrorSource.pointer`). Cross-document rules live only in `JsonApiDocumentValidator`.
42+
Pointer validation is syntax-only and does not resolve against a document; see [conformance](../docs/conformance.md).
4143
- **Identity uniqueness:** Duplicate detection is representation-strict (`ResourceObject.equals`) and alias-aware for identifier collections after id↔lid binding.
4244
- **Wire vocabulary:** `JsonApiMembers` holds shared JSON:API member-name constants for codecs and reserved-name sets; it is not an application-facing entry point.
4345
- **Diagnostics:** Failures use `JsonApiValidationException` with a stable `ValidationRuleCode` and a JSON Pointer-like path—not bare `IllegalArgumentException`.

‎jsonapi-java-core/src/main/java/io/github/kazemek/jsonapi/core/internal/SyntaxValidators.java‎

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
import java.util.regex.Pattern;
66
import org.jspecify.annotations.Nullable;
77

8-
/** Syntax validation for URI references, link relations, language tags, and media types. */
8+
/**
9+
* Syntax validation for URI references, link relations, language tags, media types, and JSON
10+
* Pointers.
11+
*/
912
public final class SyntaxValidators {
1013

1114
/** RFC 7230 tchar: token characters for media-type type/subtype and parameter names/values. */
@@ -100,6 +103,38 @@ public static boolean isValidExtensionOrProfileUri(@Nullable String value) {
100103
return parseUriReference(value, true);
101104
}
102105

106+
/**
107+
* RFC 6901 JSON Pointer syntax only. Empty string is valid; {@code null} is not. Does not resolve
108+
* against a document. URI-fragment form ({@code #/…}) is rejected.
109+
*/
110+
public static boolean isValidJsonPointer(@Nullable String value) {
111+
if (value == null) {
112+
return false;
113+
}
114+
if (value.isEmpty()) {
115+
return true;
116+
}
117+
if (value.charAt(0) != '/') {
118+
return false;
119+
}
120+
int i = 0;
121+
while (i < value.length()) {
122+
if (value.charAt(i) != '~') {
123+
i++;
124+
continue;
125+
}
126+
if (i + 1 >= value.length()) {
127+
return false;
128+
}
129+
char escape = value.charAt(i + 1);
130+
if (escape != '0' && escape != '1') {
131+
return false;
132+
}
133+
i += 2;
134+
}
135+
return true;
136+
}
137+
103138
/**
104139
* Parses an RFC 3986 URI-reference. When {@code absoluteOnly} is true, requires {@code scheme ":"
105140
* hier-part}.

‎jsonapi-java-core/src/main/java/io/github/kazemek/jsonapi/core/internal/package-info.java‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
/**
22
* Shared implementation helpers for the core document model and validation.
33
*
4-
* <p>This package is not a public API. It hosts URI/media-type/link syntax validators, ordered
5-
* null-preserving collection copies, and additional-member copy helpers used by {@code core.model}
6-
* and {@code core.validation}. See ADR-009 for nullness policy.
4+
* <p>This package is not a public API. It hosts URI/media-type/link/JSON Pointer syntax validators,
5+
* ordered null-preserving collection copies, and additional-member copy helpers used by {@code
6+
* core.model} and {@code core.validation}. Inbound JSON Pointer syntax lives in {@link
7+
* io.github.kazemek.jsonapi.core.internal.SyntaxValidators}; {@link
8+
* io.github.kazemek.jsonapi.core.internal.JsonPointers} is emit/escape only. See ADR-009 for
9+
* nullness policy.
710
*/
811
@NullMarked
912
package io.github.kazemek.jsonapi.core.internal;

‎jsonapi-java-core/src/main/java/io/github/kazemek/jsonapi/core/model/ErrorSource.java‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
package io.github.kazemek.jsonapi.core.model;
22

33
import io.github.kazemek.jsonapi.core.internal.AdditionalMembers;
4+
import io.github.kazemek.jsonapi.core.internal.SyntaxValidators;
5+
import io.github.kazemek.jsonapi.core.validation.LocalValidation;
6+
import io.github.kazemek.jsonapi.core.validation.ValidationRuleCode;
47
import java.util.Map;
58
import java.util.Set;
69
import org.jspecify.annotations.Nullable;
710

8-
/** Error object source pointer. */
11+
/**
12+
* Error object {@code source} members: optional JSON Pointer, parameter name, and header name.
13+
*
14+
* <p>When present, {@code pointer} must be RFC 6901 JSON Pointer syntax (syntax only; not resolved
15+
* against a document). See {@code docs/conformance.md}.
16+
*/
917
public record ErrorSource(
1018
@Nullable String pointer,
1119
@Nullable String parameter,
@@ -16,6 +24,12 @@ public record ErrorSource(
1624
Set.of(JsonApiMembers.POINTER, JsonApiMembers.PARAMETER, JsonApiMembers.HEADER);
1725

1826
public ErrorSource {
27+
if (pointer != null && !SyntaxValidators.isValidJsonPointer(pointer)) {
28+
LocalValidation.fail(
29+
ValidationRuleCode.INVALID_JSON_POINTER,
30+
"/errors/source/pointer",
31+
"Invalid JSON Pointer: " + pointer);
32+
}
1933
additionalMembers =
2034
AdditionalMembers.copy(
2135
additionalMembers,

‎jsonapi-java-core/src/main/java/io/github/kazemek/jsonapi/core/validation/ValidationRuleCode.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public enum ValidationRuleCode {
1616
INVALID_EXTENSION_URI,
1717
INVALID_PROFILE_URI,
1818
INVALID_OPEN_JSON_VALUE,
19+
INVALID_JSON_POINTER,
1920
MEMBER_NAME_COLLISION,
2021
RESERVED_FIELD_NAME,
2122
MISSING_RESOURCE_TYPE,

0 commit comments

Comments
 (0)