Skip to content

Strengthen JSON:API JSON Schema: full reserved-name handling + single source of truth for the attribute/relationship split #8322

@soyuka

Description

@soyuka

Summary

The JSON:API JSON Schema generator (ApiPlatform\JsonApi\JsonSchema\SchemaFactory) reconstructs the document shape independently from the runtime serializer. Two related weaknesses make the generated schema drift from the actual response. Surfaced while reviewing #8313 (and complementary to #8321, which fixes the lower-risk relationship-shape bugs). These are behavior-shifting, so targeting main / the next minor.

1. Reserved attribute names are only half-handled

buildDefinitionPropertiesSchema() renames a property named id to _id in attributes, hard-coded:

if ('id' === $propertyName) {
    $attributes['_id'] = $property;
    continue;
}

But the runtime ReservedAttributeNameConverter renames five names:

public const JSON_API_RESERVED_ATTRIBUTES = [
    'id' => '_id', 'type' => '_type', 'links' => '_links',
    'relationships' => '_relationships', 'included' => '_included',
];

The base JSON schema is built with the generic api_platform.name_converter (see json_schema.php), not the reserved converter (wired only into the JSON:API normalizers in jsonapi.php). So a resource with a scalar property named type (a common field name) is documented as attributes.properties.type, while the response emits attributes._type. The documented key never matches the payload — the same doc/response mismatch #8308 was about, for four of the five reserved names.

Direction: map attribute names through ReservedAttributeNameConverter::JSON_API_RESERVED_ATTRIBUTES (single source of truth) instead of special-casing id. The relationship key rename (a relation literally named relationships/included) should be handled too.

2. (Altitude) The attribute/relationship split is duplicated from the normalizer

SchemaFactory::getRelationship() recomputes "is this property a relationship" from getNativeType() / getBuiltinTypes() — a near-verbatim copy of the runtime split in ItemNormalizer. Doc-time and runtime decide the attributes-vs-relationships partition through two independent implementations. Any change to one (new collection handling, union/intersection types, a TypeInfo migration) silently diverges the generated schema from the response — which is the root mechanism behind the whole #8308 class of bugs.

Direction: derive the schema's attribute/relationship split (and reserved-name mapping) from the same component(s) that produce the runtime document, so they cannot drift.

Out of scope but related (low priority)

Found in the same review, intentionally not fixed in #8321 (low value / cross-cutting risk):

  • Orphaned base definition. After fix(jsonapi): exclude relations from openapi attributes schema #8313 nothing references the plain #/definitions/<Class> schema, yet it still ships in components.schemas — an unused component (and still the relations-as-objects/bare-id shape the fix removed from attributes).
  • $builtSchema shared factory state. The instance cache is never reset; per-operation schemas built on the shared service can be internally incomplete (mitigated in OpenAPI output because OpenApiFactory merges every build's definitions, so the merged document is not left dangling).

Acceptance

  • A resource property named any of type/links/relationships/included is documented under the same key the response uses.
  • Schema attribute/relationship split provably matches ItemNormalizer output (shared derivation or a contract test over the fixtures).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions