Skip to content

Conversation

ttskch
Copy link
Contributor

@ttskch ttskch commented Sep 22, 2025

Q A
Branch? 4.2
Tickets Related to #6960
License MIT
Doc PR N/A

This PR makes @id and @type properties required only in the JSON-LD schema for output.

@ttskch ttskch force-pushed the fix/jsonschema-jsonld branch from 6ea2a72 to ff5e438 Compare September 22, 2025 14:18
@ttskch ttskch marked this pull request as draft September 22, 2025 15:17
@ttskch ttskch changed the title refactor(jsonschema/jsonld): remove unnecessary codes fix(jsonschema/jsonld): force input schema to json format instead of jsonld format Sep 22, 2025
@ttskch ttskch marked this pull request as ready for review September 22, 2025 16:01
@soyuka
Copy link
Member

soyuka commented Sep 26, 2025

Interesting, but its possible when using JSON-LD to have @id or @context (though they're not required). They'll not appear in the schema anymore from what I read but are they allowed?

@ttskch
Copy link
Contributor Author

ttskch commented Sep 26, 2025

@soyuka

its possible when using JSON-LD to have @id or @context (though they're not required)

I see. If so, when using JSON-LD, @id and @type should be required in the output schema, and they should be optional in the input schema. This means we'll need to generate two schemas like Book.jsonld.output and Book.jsonld.input (as I suggested in #6485).

Would it be ok to modify this PR like that?

@ttskch ttskch force-pushed the fix/jsonschema-jsonld branch from 834d183 to 638ad92 Compare September 26, 2025 18:52
@ttskch
Copy link
Contributor Author

ttskch commented Sep 26, 2025

like this. (Just a PoC, don't merge yet.)

@ttskch
Copy link
Contributor Author

ttskch commented Sep 26, 2025

Example of output result

1 2

openapi.json

{
    "components": {
        "schemas": {
            "Book": {
                "type": "object",
                "properties": {
                    "id": {
                        "readOnly": true,
                        "type": "integer"
                    },
                    "title": {
                        "type": "string"
                    }
                },
                "required": [
                    "title"
                ]
            },
            "Book.jsonld.input": {
                "allOf": [
                    {
                        "$ref": "#/components/schemas/HydraItemBaseSchema"
                    },
                    {
                        "$ref": "#/components/schemas/Book"
                    }
                ]
            },
            "Book.jsonld.output": {
                "allOf": [
                    {
                        "$ref": "#/components/schemas/HydraOutputBaseSchema"
                    },
                    {
                        "$ref": "#/components/schemas/Book"
                    }
                ]
            },
            "HydraItemBaseSchema": {
                "type": "object",
                "properties": {
                    "@context": {
                        "oneOf": [
                            {
                                "type": "string"
                            },
                            {
                                "type": "object",
                                "properties": {
                                    "@vocab": {
                                        "type": "string"
                                    },
                                    "hydra": {
                                        "type": "string",
                                        "enum": [
                                            "http://www.w3.org/ns/hydra/core#"
                                        ]
                                    }
                                },
                                "required": [
                                    "@vocab",
                                    "hydra"
                                ],
                                "additionalProperties": true
                            }
                        ]
                    },
                    "@id": {
                        "type": "string"
                    },
                    "@type": {
                        "type": "string"
                    }
                }
            },
            "HydraOutputBaseSchema": {
                "required": [
                    "@id",
                    "@type"
                ],
                "type": "object",
                "properties": {
                    "@context": {
                        "oneOf": [
                            {
                                "type": "string"
                            },
                            {
                                "type": "object",
                                "properties": {
                                    "@vocab": {
                                        "type": "string"
                                    },
                                    "hydra": {
                                        "type": "string",
                                        "enum": [
                                            "http://www.w3.org/ns/hydra/core#"
                                        ]
                                    }
                                },
                                "required": [
                                    "@vocab",
                                    "hydra"
                                ],
                                "additionalProperties": true
                            }
                        ]
                    },
                    "@id": {
                        "type": "string"
                    },
                    "@type": {
                        "type": "string"
                    }
                }
            }
        }
    }
}

Reproducer

https://github.com/ttskch/api-platform-core-7397

@soyuka
Copy link
Member

soyuka commented Sep 27, 2025

Nice this looks fine! Can the output stay Book.jsonld?

@ttskch ttskch changed the title fix(jsonschema/jsonld): force input schema to json format instead of jsonld format fix(jsonschema/jsonld): make @id and @type properties required only in the JSON-LD schema for output Sep 27, 2025
@ttskch
Copy link
Contributor Author

ttskch commented Sep 27, 2025

@soyuka I fixed in 479f46b and updated the PR title and description.

Example of output result

1 2

openapi.json

{
    "openapi": "3.1.0",
    "info": {
        "title": "Hello API Platform",
        "description": "",
        "version": "1.0.0"
    },
    "servers": [
        {
            "url": "/",
            "description": ""
        }
    ],
    "paths": {
        "/api/books": {
            "get": {
                "operationId": "api_books_get_collection",
                "tags": [
                    "Book"
                ],
                "responses": {
                    "200": {
                        "description": "Book collection",
                        "content": {
                            "application/ld+json": {
                                "schema": {
                                    "type": "object",
                                    "description": "Book.jsonld collection.",
                                    "allOf": [
                                        {
                                            "$ref": "#/components/schemas/HydraCollectionBaseSchema"
                                        },
                                        {
                                            "type": "object",
                                            "properties": {
                                                "hydra:member": {
                                                    "type": "array",
                                                    "items": {
                                                        "$ref": "#/components/schemas/Book.jsonld"
                                                    }
                                                }
                                            }
                                        }
                                    ]
                                }
                            }
                        }
                    }
                },
                "summary": "Retrieves the collection of Book resources.",
                "description": "Retrieves the collection of Book resources.",
                "parameters": [
                    {
                        "name": "page",
                        "in": "query",
                        "description": "The collection page number",
                        "required": false,
                        "deprecated": false,
                        "schema": {
                            "type": "integer",
                            "default": 1
                        },
                        "style": "form",
                        "explode": false
                    }
                ]
            },
            "post": {
                "operationId": "api_books_post",
                "tags": [
                    "Book"
                ],
                "responses": {
                    "201": {
                        "description": "Book resource created",
                        "content": {
                            "application/ld+json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Book.jsonld"
                                }
                            }
                        },
                        "links": {}
                    },
                    "400": {
                        "description": "Invalid input",
                        "content": {
                            "application/ld+json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error.jsonld"
                                }
                            },
                            "application/problem+json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            },
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        },
                        "links": {}
                    },
                    "422": {
                        "description": "An error occurred",
                        "content": {
                            "application/ld+json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ConstraintViolation.jsonld"
                                }
                            },
                            "application/problem+json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ConstraintViolation"
                                }
                            },
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ConstraintViolation"
                                }
                            }
                        },
                        "links": {}
                    }
                },
                "summary": "Creates a Book resource.",
                "description": "Creates a Book resource.",
                "parameters": [],
                "requestBody": {
                    "description": "The new Book resource",
                    "content": {
                        "application/ld+json": {
                            "schema": {
                                "$ref": "#/components/schemas/Book.jsonld.input"
                            }
                        }
                    },
                    "required": true
                }
            }
        },
        "/api/books/{id}": {
            "get": {
                "operationId": "api_books_id_get",
                "tags": [
                    "Book"
                ],
                "responses": {
                    "200": {
                        "description": "Book resource",
                        "content": {
                            "application/ld+json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Book.jsonld"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Not found",
                        "content": {
                            "application/ld+json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error.jsonld"
                                }
                            },
                            "application/problem+json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            },
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        },
                        "links": {}
                    }
                },
                "summary": "Retrieves a Book resource.",
                "description": "Retrieves a Book resource.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "description": "Book identifier",
                        "required": true,
                        "deprecated": false,
                        "schema": {
                            "type": "string"
                        },
                        "style": "simple",
                        "explode": false
                    }
                ]
            },
            "delete": {
                "operationId": "api_books_id_delete",
                "tags": [
                    "Book"
                ],
                "responses": {
                    "204": {
                        "description": "Book resource deleted"
                    },
                    "404": {
                        "description": "Not found",
                        "content": {
                            "application/ld+json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error.jsonld"
                                }
                            },
                            "application/problem+json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            },
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        },
                        "links": {}
                    }
                },
                "summary": "Removes the Book resource.",
                "description": "Removes the Book resource.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "description": "Book identifier",
                        "required": true,
                        "deprecated": false,
                        "schema": {
                            "type": "string"
                        },
                        "style": "simple",
                        "explode": false
                    }
                ]
            },
            "patch": {
                "operationId": "api_books_id_patch",
                "tags": [
                    "Book"
                ],
                "responses": {
                    "200": {
                        "description": "Book resource updated",
                        "content": {
                            "application/ld+json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Book.jsonld"
                                }
                            }
                        },
                        "links": {}
                    },
                    "400": {
                        "description": "Invalid input",
                        "content": {
                            "application/ld+json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error.jsonld"
                                }
                            },
                            "application/problem+json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            },
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        },
                        "links": {}
                    },
                    "422": {
                        "description": "An error occurred",
                        "content": {
                            "application/ld+json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ConstraintViolation.jsonld"
                                }
                            },
                            "application/problem+json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ConstraintViolation"
                                }
                            },
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/ConstraintViolation"
                                }
                            }
                        },
                        "links": {}
                    },
                    "404": {
                        "description": "Not found",
                        "content": {
                            "application/ld+json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error.jsonld"
                                }
                            },
                            "application/problem+json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            },
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                }
                            }
                        },
                        "links": {}
                    }
                },
                "summary": "Updates the Book resource.",
                "description": "Updates the Book resource.",
                "parameters": [
                    {
                        "name": "id",
                        "in": "path",
                        "description": "Book identifier",
                        "required": true,
                        "deprecated": false,
                        "schema": {
                            "type": "string"
                        },
                        "style": "simple",
                        "explode": false
                    }
                ],
                "requestBody": {
                    "description": "The updated Book resource",
                    "content": {
                        "application/merge-patch+json": {
                            "schema": {
                                "$ref": "#/components/schemas/Book"
                            }
                        }
                    },
                    "required": true
                }
            }
        }
    },
    "components": {
        "schemas": {
            "Book": {
                "type": "object",
                "properties": {
                    "id": {
                        "readOnly": true,
                        "type": "integer"
                    },
                    "title": {
                        "type": "string"
                    }
                },
                "required": [
                    "title"
                ]
            },
            "Book.jsonld": {
                "allOf": [
                    {
                        "$ref": "#/components/schemas/HydraOutputBaseSchema"
                    },
                    {
                        "$ref": "#/components/schemas/Book"
                    }
                ]
            },
            "Book.jsonld.input": {
                "allOf": [
                    {
                        "$ref": "#/components/schemas/HydraItemBaseSchema"
                    },
                    {
                        "$ref": "#/components/schemas/Book"
                    }
                ]
            },
            "ConstraintViolation": {
                "type": "object",
                "description": "Unprocessable entity",
                "properties": {
                    "status": {
                        "default": 422,
                        "type": "integer"
                    },
                    "violations": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "propertyPath": {
                                    "type": "string",
                                    "description": "The property path of the violation"
                                },
                                "message": {
                                    "type": "string",
                                    "description": "The message associated with the violation"
                                }
                            }
                        }
                    },
                    "detail": {
                        "readOnly": true,
                        "type": "string"
                    },
                    "type": {
                        "readOnly": true,
                        "type": "string"
                    },
                    "title": {
                        "readOnly": true,
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "instance": {
                        "readOnly": true,
                        "type": [
                            "string",
                            "null"
                        ]
                    }
                }
            },
            "ConstraintViolation.jsonld": {
                "allOf": [
                    {
                        "$ref": "#/components/schemas/HydraOutputBaseSchema"
                    },
                    {
                        "$ref": "#/components/schemas/ConstraintViolation"
                    }
                ]
            },
            "Error": {
                "type": "object",
                "description": "A representation of common errors.",
                "properties": {
                    "title": {
                        "readOnly": true,
                        "description": "A short, human-readable summary of the problem.",
                        "type": "string"
                    },
                    "detail": {
                        "readOnly": true,
                        "description": "A human-readable explanation specific to this occurrence of the problem.",
                        "type": "string"
                    },
                    "status": {
                        "type": "number",
                        "examples": [
                            404
                        ],
                        "default": 400
                    },
                    "instance": {
                        "readOnly": true,
                        "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced.",
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "type": {
                        "readOnly": true,
                        "description": "A URI reference that identifies the problem type",
                        "type": "string"
                    }
                }
            },
            "Error.jsonld": {
                "allOf": [
                    {
                        "$ref": "#/components/schemas/HydraOutputBaseSchema"
                    },
                    {
                        "$ref": "#/components/schemas/Error"
                    }
                ]
            },
            "HydraCollectionBaseSchema": {
                "type": "object",
                "required": [
                    "hydra:member"
                ],
                "properties": {
                    "hydra:member": {
                        "type": "array"
                    },
                    "hydra:totalItems": {
                        "type": "integer",
                        "minimum": 0
                    },
                    "hydra:view": {
                        "type": "object",
                        "properties": {
                            "@id": {
                                "type": "string",
                                "format": "iri-reference"
                            },
                            "@type": {
                                "type": "string"
                            },
                            "hydra:first": {
                                "type": "string",
                                "format": "iri-reference"
                            },
                            "hydra:last": {
                                "type": "string",
                                "format": "iri-reference"
                            },
                            "hydra:previous": {
                                "type": "string",
                                "format": "iri-reference"
                            },
                            "hydra:next": {
                                "type": "string",
                                "format": "iri-reference"
                            }
                        },
                        "example": {
                            "@id": "string",
                            "type": "string",
                            "hydra:first": "string",
                            "hydra:last": "string",
                            "hydra:previous": "string",
                            "hydra:next": "string"
                        }
                    },
                    "hydra:search": {
                        "type": "object",
                        "properties": {
                            "@type": {
                                "type": "string"
                            },
                            "hydra:template": {
                                "type": "string"
                            },
                            "hydra:variableRepresentation": {
                                "type": "string"
                            },
                            "hydra:mapping": {
                                "type": "array",
                                "items": {
                                    "type": "object",
                                    "properties": {
                                        "@type": {
                                            "type": "string"
                                        },
                                        "variable": {
                                            "type": "string"
                                        },
                                        "property": {
                                            "type": [
                                                "string",
                                                "null"
                                            ]
                                        },
                                        "required": {
                                            "type": "boolean"
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            },
            "HydraItemBaseSchema": {
                "type": "object",
                "properties": {
                    "@context": {
                        "oneOf": [
                            {
                                "type": "string"
                            },
                            {
                                "type": "object",
                                "properties": {
                                    "@vocab": {
                                        "type": "string"
                                    },
                                    "hydra": {
                                        "type": "string",
                                        "enum": [
                                            "http://www.w3.org/ns/hydra/core#"
                                        ]
                                    }
                                },
                                "required": [
                                    "@vocab",
                                    "hydra"
                                ],
                                "additionalProperties": true
                            }
                        ]
                    },
                    "@id": {
                        "type": "string"
                    },
                    "@type": {
                        "type": "string"
                    }
                }
            },
            "HydraOutputBaseSchema": {
                "required": [
                    "@id",
                    "@type"
                ],
                "type": "object",
                "properties": {
                    "@context": {
                        "oneOf": [
                            {
                                "type": "string"
                            },
                            {
                                "type": "object",
                                "properties": {
                                    "@vocab": {
                                        "type": "string"
                                    },
                                    "hydra": {
                                        "type": "string",
                                        "enum": [
                                            "http://www.w3.org/ns/hydra/core#"
                                        ]
                                    }
                                },
                                "required": [
                                    "@vocab",
                                    "hydra"
                                ],
                                "additionalProperties": true
                            }
                        ]
                    },
                    "@id": {
                        "type": "string"
                    },
                    "@type": {
                        "type": "string"
                    }
                }
            }
        },
        "responses": {},
        "parameters": {},
        "examples": {},
        "requestBodies": {},
        "headers": {},
        "securitySchemes": {}
    },
    "security": [],
    "tags": [
        {
            "name": "Book",
            "description": "Resource 'Book' operations."
        }
    ],
    "webhooks": {}
}

Reproducer

https://github.com/ttskch/api-platform-core-7397

@ttskch
Copy link
Contributor Author

ttskch commented Sep 27, 2025

I'm still working to fix the CI error.

@ttskch ttskch force-pushed the fix/jsonschema-jsonld branch 2 times, most recently from 1cda6cf to 5fdaa77 Compare September 27, 2025 15:17
@soyuka
Copy link
Member

soyuka commented Sep 27, 2025

I can take a look if you want, and I may move this test to phpunit I thought that keeping Book.jsonld would do the trick but its probably looking for Book.jsonld-input ^^. Let me know if I should fix this.

@soyuka soyuka force-pushed the fix/jsonschema-jsonld branch from cb98a29 to b0cac3a Compare September 29, 2025 09:16
$definitionName .= '.input';
}

$jsonSchema = $this->schemaFactory->buildSchema($className, 'json', $type, $operation, new Schema(version: $schema->getVersion()), $serializerContext, $forceCollection);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't work as references will be with json instead of jsonld

Copy link
Contributor Author

@ttskch ttskch Sep 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@soyuka What do you mean?🤔 I understand that the intention of your fix in #6960 was to reference a json schema in the definition of a jsonld schema. I completely agree with that intention, but unfortunately #6960 had the following bugs:

  • The output schema was applied even on input, making @id and @type required.
  • The json schema was not referenced (only HydraItemBaseSchema and HydraCollectionBaseSchema were referenced).

This PR fixes these two bugs.

In order to reference or embed a json schema (e.g., Book instead of Book.jsonld), I believe you need to explicitly create a json schema here.

@soyuka soyuka force-pushed the fix/jsonschema-jsonld branch from b0cac3a to 5fdaa77 Compare September 29, 2025 09:32
public function buildSchema(string $className, string $format = 'jsonld', string $type = Schema::TYPE_OUTPUT, ?Operation $operation = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
{
if ('jsonld' !== $format || 'input' === $type) {
if ('jsonld' !== $format) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm quite unsure about this as when its an input we were going through this path making @id required only on output

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@soyuka If we add 'input' === $type here, this method will not be executed at all during input, and Book.jsonld.input will not be generated.

The @id @type @context properties will be added during both input and output, and @id @type should only be required during output. This control is done here and here.

@ttskch ttskch force-pushed the fix/jsonschema-jsonld branch from 53e1e96 to fa1305f Compare September 30, 2025 02:17
…ferencing it

It cannot always be referenced, as there may be resources that don't have a JSON schema but only a JSON-LD schema.
It's not certain at the time the JSON-LD schema is being generated whether a JSON schema will ultimately be defined.
Therefore, the JSON schema will be referenced only if it's already defined.
@ttskch ttskch force-pushed the fix/jsonschema-jsonld branch from fa1305f to ad9be8a Compare September 30, 2025 02:25
@ttskch
Copy link
Contributor Author

ttskch commented Sep 30, 2025

rebased with current 4.2

@ttskch ttskch force-pushed the fix/jsonschema-jsonld branch from e92aeff to d1e60d9 Compare September 30, 2025 04:22
@ttskch
Copy link
Contributor Author

ttskch commented Sep 30, 2025

@soyuka I've mostly revised the existing tests to match the implementation. The only thing I'm unsure of is how to properly modify testSchemaIsNotValid() and testSchemaIsValid() in JsonSchemaTest.

The new schema uses $ref references, so I don't know how to properly handle this in jsonrainbow/json-schema. I tried to follow the README, but it didn't work😓 Do you have any ideas?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants