Skip to content

Commit abc39ee

Browse files
authored
feat(core): complete JSON:API create-request validation (#147)
Establish the authoritative base-spec create-request contract in core validation before the Level-1 application API consumes it.
1 parent 3ac3733 commit abc39ee

8 files changed

Lines changed: 345 additions & 20 deletions

File tree

‎docs/conformance.md‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Conformance is reported per feature as: **supported**, **pass-through**, **delegated**, **deferred**, or **out of scope**.
44

5-
Current capability: `jsonapi-java-core` owns the document model, aggregate validation, update-request
5+
Current capability: `jsonapi-java-core` owns the document model, aggregate validation, create- and update-request
66
shape, error `source.pointer` syntax, and reserved link names. `jsonapi-java-annotations` owns
77
metadata-only domain-mapping annotations. `jsonapi-java-jackson3` owns the Jackson 3 document
88
writer/reader, domain-to-resource mapping, compound inclusion, sparse fieldsets, flat DTO binding,
@@ -80,6 +80,19 @@ Spring adapters remain deferred.
8080
| Command application | out of scope | Applications apply authorized update commands; Jackson 2 binding remains deferred |
8181
| HTTP/route identity derivation and mutation | out of scope | Application-owned; core compares only a supplied expected identity |
8282

83+
## Resource create request validation (supported)
84+
85+
| Rule | Status | Notes |
86+
|-----------------------------------------------------------------------------------------------------------------|--------------|---------------------------------------------------------------------------------------------------------------------------|
87+
| Create primary data must be one resource object (absent, null, collection, or identifier primary data rejected) | supported | `CREATE_REQUIRES_SINGLE_RESOURCE` at `/data` |
88+
| Create resource `id` optional; `id` and `lid` remain independent | supported | Aggregate validator; neither substitutes for the other; empty/whitespace strings are present |
89+
| Every relationship supplied on the primary create resource must contain `data` | supported | Reuses `RELATIONSHIP_DATA_REQUIRED` at `/data/relationships/<name>/data`; primary resource only |
90+
| Relationship linkage preserved: null, single, empty and non-empty collection | supported | All `RelationshipData` variants valid, including `lid`-based linkage |
91+
| Omitted/present-empty relationship wrappers; links/meta coexist with present linkage | supported | Absent vs `Relationships.empty()`; no normalization |
92+
| Create rules scoped to the primary resource | supported | `included` resources are exempt from the primary relationship-data rule and gain no nested-create interpretation; full linkage still enforced; pre-existing id-optional identity leniency is unchanged document-wide |
93+
| Links-only/meta-only relationships outside create-specific restrictions | supported | Valid general core/document representations per ADR-018; rejected only on the primary create resource |
94+
| HTTP/method handling and mutation | out of scope | Application-owned; a future Spring layer selects `CREATE_REQUEST` from its own operation context; core stays method-neutral |
95+
8396
## Annotation metadata (supported)
8497

8598
| Rule | Status | Notes |

‎jsonapi-java-core/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ JsonApiDocument document = JsonApiDocument.withData(
2020
new JsonApiDocumentValidator().validate(document, ValidationContext.defaults());
2121
```
2222

23-
Construct model types first (local invariants run in constructors). Call `JsonApiDocumentValidator` with a `ValidationContext` for rules that need the whole document (identity uniqueness, full linkage, extension/profile policy, and similar). For update requests use `DocumentUsage.UPDATE_REQUEST`; a `withExpectedEndpointIdentity(EndpointIdentity)` context makes the validator compare the primary resource `type`+`id` against a caller-derived expected endpoint identity. HTTP/route derivation and mutation remain application-owned.
23+
Construct model types first (local invariants run in constructors). Call `JsonApiDocumentValidator` with a `ValidationContext` for rules that need the whole document (identity uniqueness, full linkage, extension/profile policy, and similar). For create requests use `DocumentUsage.CREATE_REQUEST`: primary data must be a single resource object whose `id` may be omitted (`id` and `lid` stay independent), and every relationship supplied on that resource must contain `data` (null, single, and collection linkage all remain valid). For update requests use `DocumentUsage.UPDATE_REQUEST`; a `withExpectedEndpointIdentity(EndpointIdentity)` context makes the validator compare the primary resource `type`+`id` against a caller-derived expected endpoint identity. Included resources are exempt from the primary-resource relationship-data rule under both write usages; otherwise existing identity and aggregate rules apply unchanged. HTTP/route derivation and mutation remain application-owned.
2424

2525
## Non-goals
2626

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@
22

33
/** Declares how a document is used for context-sensitive validation. */
44
public enum DocumentUsage {
5+
/**
6+
* Base-spec create-resource request: primary data must be a single resource object whose {@code
7+
* id} may be omitted, and every relationship supplied on that resource must contain {@code data}.
8+
* Included resources are exempt from the primary relationship-data rule; otherwise existing rules
9+
* apply unchanged. Core itself is not HTTP-method-aware; a future server layer selects this usage
10+
* from its own operation context.
11+
*/
512
CREATE_REQUEST,
13+
/** Base-spec update request: single-resource primary data with required {@code id}. */
614
UPDATE_REQUEST,
15+
/** Response or any other document use. */
716
RESPONSE_OR_OTHER
817
}

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

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ public void validate(JsonApiDocument document, ValidationContext context) {
7272
PATH_DATA,
7373
"Update request requires primary data as a single resource object");
7474
}
75+
if (context.documentUsage() == DocumentUsage.CREATE_REQUEST) {
76+
throw new JsonApiValidationException(
77+
ValidationRuleCode.CREATE_REQUIRES_SINGLE_RESOURCE,
78+
PATH_DATA,
79+
"Create request requires primary data as a single resource object");
80+
}
7581
} else {
7682
validatePrimaryData(document.data(), context);
7783
}
@@ -100,6 +106,13 @@ private void validatePrimaryData(@Nullable DocumentData data, ValidationContext
100106
PATH_DATA,
101107
"Update request requires primary data as a single resource object");
102108
}
109+
if (context.documentUsage() == DocumentUsage.CREATE_REQUEST
110+
&& !(data instanceof DocumentData.SingleResource)) {
111+
throw new JsonApiValidationException(
112+
ValidationRuleCode.CREATE_REQUIRES_SINGLE_RESOURCE,
113+
PATH_DATA,
114+
"Create request requires primary data as a single resource object");
115+
}
103116
switch (data) {
104117
case DocumentData.NullData ignored -> {
105118
// Explicit null primary data has no nested members to validate.
@@ -149,20 +162,32 @@ private void validateResource(
149162
validateAdditionalMembers(resource.additionalMembers(), path, context);
150163
}
151164

165+
/**
166+
* Operation usages whose primary-resource relationships must carry replacement linkage {@code
167+
* data}. This is an operation-specific validation policy: links-only and meta-only relationships
168+
* remain valid general document representations outside these usages.
169+
*/
170+
private static boolean isWriteRequestRequiringRelationshipData(ValidationContext context) {
171+
return context.documentUsage() == DocumentUsage.UPDATE_REQUEST
172+
|| context.documentUsage() == DocumentUsage.CREATE_REQUEST;
173+
}
174+
152175
private void validateResourceRelationships(
153176
ResourceObject resource, String path, boolean primary, ValidationContext context) {
154177
Relationships relationships = Objects.requireNonNull(resource.relationships());
155178
validateAdditionalMembers(
156179
relationships.additionalMembers(), path + PATH_RELATIONSHIPS, context);
157180
for (Map.Entry<String, Relationship> entry : relationships.relationships().entrySet()) {
158181
Relationship relationship = entry.getValue();
159-
if (context.documentUsage() == DocumentUsage.UPDATE_REQUEST
182+
if (isWriteRequestRequiringRelationshipData(context)
160183
&& primary
161184
&& !relationship.hasDataMember()) {
162185
throw new JsonApiValidationException(
163186
ValidationRuleCode.RELATIONSHIP_DATA_REQUIRED,
164187
JsonPointers.child(path + PATH_RELATIONSHIPS, entry.getKey()) + PATH_DATA,
165-
"Update request relationship must contain data");
188+
context.documentUsage() == DocumentUsage.CREATE_REQUEST
189+
? "Create request relationship must contain data"
190+
: "Update request relationship must contain data");
166191
}
167192
validateRelationship(
168193
relationship,

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,6 @@ public enum ValidationRuleCode {
4242
NULL_COLLECTION_PAYLOAD,
4343
NULL_COLLECTION_ELEMENT,
4444
NULL_REQUIRED_VALUE,
45-
UPDATE_REQUIRES_SINGLE_RESOURCE
45+
UPDATE_REQUIRES_SINGLE_RESOURCE,
46+
CREATE_REQUIRES_SINGLE_RESOURCE
4647
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@
1212
* single-resource primary data, replacement {@code data} on every relationship supplied by the
1313
* primary resource, and — when an {@link
1414
* io.github.kazemek.jsonapi.core.validation.EndpointIdentity} is configured — a primary resource
15-
* identity matching the expected endpoint. Included resources keep response semantics.
15+
* identity matching the expected endpoint. {@link
16+
* io.github.kazemek.jsonapi.core.validation.DocumentUsage#CREATE_REQUEST} requires single-resource
17+
* primary data, permits an omitted resource {@code id} with {@code id} and {@code lid} kept
18+
* independent, and requires {@code data} on every relationship supplied by the primary resource
19+
* while accepting null, single, and collection linkage. Included resources are exempt from the
20+
* primary-resource relationship-data rule under both write usages; otherwise existing identity and
21+
* aggregate rules apply unchanged.
1622
*
1723
* <p>Failures carry a stable {@link io.github.kazemek.jsonapi.core.validation.ValidationRuleCode}
1824
* and a JSON Pointer-like path. See ADR-003, ADR-009, ADR-012, and {@code docs/conformance.md}.

0 commit comments

Comments
 (0)