feat(jsonapi): allow opt-in client-generated IDs on POST per spec#7930
feat(jsonapi): allow opt-in client-generated IDs on POST per spec#7930abderrahimghazali wants to merge 4 commits intoapi-platform:mainfrom
Conversation
| // Per JSON:API spec, `id` is optional in the request body of a creation: | ||
| // https://jsonapi.org/format/#crud-creating | ||
| $required = ['type', 'id']; | ||
| if (Schema::TYPE_INPUT === $type && $resourceOperation instanceof Post) { |
There was a problem hiding this comment.
we prefer $resourceOperation->getMethod()
There was a problem hiding this comment.
Updated, I switched to 'POST' === $resourceOperation->getMethod() and dropped the now-unused Post import. Added a null guard since $resourceOperation is typed ?Operation. Thanks! 🙂
…maFactory Address review feedback from @soyuka.
getMethod() is declared on HttpOperation, not on the abstract Operation, so PHPStan flagged the call. Use instanceof HttpOperation as the guard.
| // Avoid issues with proxies if we populated the object | ||
| if (!isset($context[self::OBJECT_TO_POPULATE]) && isset($data['data']['id'])) { | ||
| if (true !== ($context['api_allow_update'] ?? true)) { | ||
| if ($operation instanceof Post) { |
There was a problem hiding this comment.
same lets prefer the getMethod approach
|
Good alignment with the spec, we need to provide a configuration option that sets this context value, also this will be considered as a new feature and commit/pr title should be renamed accordingly. |
Address review feedback from @soyuka: - ItemNormalizer now uses HttpOperation::getMethod() instead of `instanceof Post`. - The ALLOW_CLIENT_GENERATED_ID flag can now be enabled declaratively on the operation via `extraProperties`, in addition to the denormalization context: `#[Post(extraProperties: [ItemNormalizer::ALLOW_CLIENT_GENERATED_ID => true])]`.
|
Done in 251eb81 — #[Post(extraProperties: [ItemNormalizer::ALLOW_CLIENT_GENERATED_ID => true])]Added a test covering that path. Renamed the PR title to |
What's in this PR?
Two related JSON:API issues prevent valid POST requests with client-generated IDs (per JSON:API §7.3):
SchemaFactory) declareddata.idrequiredfor every operation, including the request body of aPOST. The spec saysidMAY be supplied by the client and is otherwise optional on creation.ItemNormalizer) treated any incomingdata.idas a hint to load an existing resource, then either threwUpdate is not allowed for this operationor failed to resolve the IRI when the client passed a fresh UUID.Together they make it impossible to POST
{"data":{"type":"…","id":"<uuid>","attributes":{…}}}even when the application is designed for client-generated identifiers (Doctrine UUID PK, ULID, etc.).Fix
JsonApi\JsonSchema\SchemaFactorySchema::TYPE_INPUTon aPostoperation,data.requiredis now["type"]. Output schemas and non-Postoperations remain["type", "id"](response payloads still always carry anid).buildDefinitionPropertiesSchema()because the relationship loop reassigns\$operation.Before (POST request body):
```json
"required": ["type", "id"]
```
After:
```json
"required": ["type"]
```
JsonApi\Serializer\ItemNormalizerItemNormalizer::ALLOW_CLIENT_GENERATED_ID('allow_client_generated_id').Post, an incomingdata.idno longer triggers an existing-resource lookup.NotNormalizableValueExceptionwith a clear message — no behaviour change for endpoints that don't expect client-generated IDs, and no risk of silently letting a client spoof an ID.OBJECT_TO_POPULATE) is preserved.The flag is off by default to keep the change non-breaking and to avoid an ID-spoofing footgun on public endpoints. Applications opt in per-operation by passing the flag in the denormalization context (or via a state processor / serializer context builder).
Tests
SchemaFactoryTest::testBuildSchemaForPostInputDoesNotRequireId— POST input dropsidfromrequired.SchemaFactoryTest::testBuildSchemaForPostOutputStillRequiresId— POST output still requiresid.ItemNormalizerTest::testDenormalizePostWithIdThrowsWithoutOptIn— POST with client id throws without opt-in.ItemNormalizerTest::testDenormalizePostWithIdSucceedsWithOptIn— POST with client id succeeds with opt-in, IRI converter is not called, id is set on the new entity.Full
src/JsonApi/Testssuite: 57 tests / 145 assertions, all green (the 2 PHPUnit notices are pre-existing).Spec references
Credit to @cay89 for the original analysis in #6738.