Skip to content

Conversation

oddssoft
Copy link

@oddssoft oddssoft commented Jul 14, 2025

?string - type: [string, null] not type: string

Q A
Bugfix? ✔️/❌
Breaks BC? ✔️/❌
New feature? ✔️/❌
Issues #...
Issue: ?string field incorrectly mapped as non-nullable in JSON Schema
I attempted to generate a JSON Schema for the following DTO:

`final class CoutryDto
{
#[Field(format: Format::DateTime)]
public string $createdAt;
#[Field(format: Format::DateTime)]
public string $updatedAt;
#[Field(default: null, format: Format::DateTime)]
public ?string $deletedAt;

public function __construct(
    public string $id,
    public ?string $alpha2,
    public ?string $alpha3,
    public ?int $geonameId,
    public ?int $feedcacheId,
    public string $name,
    DateTimeImmutable $createdAt,
    DateTimeImmutable $updatedAt,
    ?DateTimeImmutable $deletedAt,
) {
    $this->createdAt = $createdAt->format(DATE_RFC3339);
    $this->updatedAt = $updatedAt->format(DATE_RFC3339);
    $this->deletedAt = $deletedAt?->format(DATE_RFC3339);
}

}`

Note: The $deletedAt property is explicitly typed as ?string and should be treated as nullable.

However, the generated schema incorrectly defines deletedAt as a non-nullable string:
{ "properties":{ "createdAt":{ "format":"date-time", "type":"string" }, "updatedAt":{ "format":"date-time", "type":"string" }, "deletedAt":{ "format":"date-time", "type":"string" }, "id":{ "format":"uuid", "type":"string" }, "name":{ "type":"string" }, "patterns":{ "type":"array", "items":{ "$ref":"#/definitions/PatternUpdated" } } }, "required":[ "createdAt", "updatedAt", "id", "name", "patterns" ], "definitions":{ "PatternUpdated":{ "title":"PatternUpdated", "type":"object", "properties":{ "id":{ "type":"string" }, "description":{ "type":"string" } }, "required":[ "id", "description" ] } } }

When validating a CountryDto instance with deletedAt = null, the following error is returned:
[ { "property": "deletedAt", "pointer": "/deletedAt", "message": "NULL value found, but a string is required", "constraint": { "name": "type", "params": { "found": "NULL", "expected": "a string" } }, "context": 1 } ]

While adding support for nullable properties in JSON Schema generation, several related issues were discovered and addressed:

✅ Added support for nullable enum types using ["string", "null"] representation

🛠️ Fixed incorrect usage of allOf for class-based properties — replaced with proper $ref or oneOf([$ref, null])

🚫 Removed deprecated type usages that triggered Psalm errors under Symfony 7.3

🧹 Improved compatibility and reduced warnings in modern environments

These changes improve both the correctness and compatibility of generated schemas and static analysis in modern Symfony setups.

@roxblnfk roxblnfk requested review from butschster and msmakouz July 14, 2025 19:40
@butschster
Copy link
Member

@oddssoft
Hey, thx for the PR. Could you check tests?

@oddssoft oddssoft changed the title add nullable type support Add nullable enum support, fix Symfony 7.3 Psalm warnings, and correct class property definitions Jul 15, 2025
@oddssoft
Copy link
Author

oddssoft commented Jul 15, 2025

@oddssoft Hey, thx for the PR. Could you check tests?

📝 Description
Initially this PR aimed to add nullable type support for scalar types, but during implementation, a few more issues had to be addressed:

✅ Added support for nullable enum types (["string", "null"])

🛠️ Fixed incorrect use of allOf for class-based properties — replaced with proper $ref or oneOf([$ref, null])

🔧 Removed deprecated types that caused Psalm errors under Symfony 7.3

⚙️ Verified compatibility with PHP 8.4 and Symfony 7.3

✅ Validation
The generated JSON Schemas were tested using test models like Actor and Movie, with different data scenarios:

fully populated objects

objects with some null fields

All schemas validated correctly via jsonschemavalidator.net.

Below is the generated JSON Schema for the Actor class, including proper handling of nullable fields and nested object references:

{ "properties": { "name": { "type": "string" }, "age": { "type": "integer" }, "bio": { "title": "Biography", "description": "The biography of the actor", "type": ["string", "null"] }, "movies": { "type": "array", "items": { "$ref": "#/definitions/Movie" }, "default": [] }, "bestMovie": { "title": "Best Movie", "description": "The best movie of the actor", "oneOf": [ { "$ref": "#/definitions/Movie" }, { "type": "null" } ] } }, "required": ["name", "age"], "definitions": { "Movie": { "title": "Movie", "type": "object", "properties": { "title": { "title": "Title", "description": "The title of the movie", "type": "string" }, "year": { "title": "Year", "description": "The year of the movie", "type": "integer" }, "description": { "title": "Description", "description": "The description of the movie", "type": ["string", "null"] }, "director": { "type": ["string", "null"] }, "releaseDate": { "type": ["string", "null"], "format": "date", "title": "Release date", "description": "The release date of the movie" }, "releaseStatus": { "title": "Release Status", "description": "The release status of the movie", "type": ["string", "null"], "enum": [ "Released", "Rumored", "Post Production", "In Production", "Planned", "Canceled" ] } }, "required": ["title", "year"] } } }
Various scenarios were tested:

Invalid Data – Fails Validation (movies must be an array of valid Movie objects — null is not allowed in the array.)

{ "name": "Nicolas Cage", "age": 60, "bio": "Iconic and intense actor.", "movies": [null], "bestMovie": null }

Minimal Valid Input. Valid: only required fields provided.

{ "name": "Tom Hardy", "age": 46, "movies": [] }

Fully Populated Input. Valid

{ "name": "Leonardo DiCaprio", "age": 49, "bio": "An Oscar-winning actor known for roles in Titanic and Inception.", "movies": [ { "title": "Inception", "year": 2010, "description": "A mind-bending thriller about dreams within dreams.", "director": "Christopher Nolan", "releaseStatus": "Released", "releaseDate": "2010-07-16" }, { "title": "The Revenant", "year": 2015, "description": "A frontiersman fights for survival in the wilderness.", "director": "Alejandro G. Iñárritu", "releaseStatus": "Released", "releaseDate": "2015-12-25" } ], "bestMovie": { "title": "The Revenant", "year": 2015, "description": "A frontiersman fights for survival in the wilderness.", "director": "Alejandro G. Iñárritu", "releaseStatus": "Released", "releaseDate": "2015-12-25" } }

Typical Input with Nullable Fields. Valid: optional fields use null values where allowed.

{ "name": "Emma Stone", "age": 35, "bio": null, "movies": [ { "title": "La La Land", "year": 2016, "description": "A jazz musician and an actress fall in love.", "director": "Damien Chazelle", "releaseStatus": "Released", "releaseDate": "2016-12-09" } ], "bestMovie": null }

@oddssoft
Copy link
Author

Hi @butschster! Could you please take another look at this PR when you have a moment?

- Changed enum generation from $ref to inline enum with "type": "string" and explicit "enum" values
- Added proper nullable support for enums using ["string", "null"]
- Replaced invalid allOf usage for object types with correct $ref
- When nullable object, used oneOf with $ref and null type
@oddssoft
Copy link
Author

Hi @butschster!

Symfony's property-info version 7.3 is not compatible with previous Symfony versions (6.4 || 7.2). I suggest that in the 1.x branch we lock the symfony/property-info dependency to <7.3, since the current code cannot be built with 7.3 due to Psalm failing on deprecation warnings.

At the same time, since I personally need support for Symfony 7.3, I’d like to propose creating a new 2.x branch that drops support for Symfony versions below 7.3 — targeting users who specifically need compatibility with Symfony 7.3+.

Let me know what your preferred direction is.

I've already pushed the required changes to the 1.x branch — please have another look at the PR when you have a chance.?

@butschster butschster changed the base branch from 1.x to 2.x July 17, 2025 09:20
@butschster butschster self-assigned this Jul 17, 2025
@butschster butschster added the enhancement New feature or request label Jul 17, 2025
@butschster butschster requested a review from roxblnfk July 17, 2025 09:21
@butschster
Copy link
Member

It would be great if someone get union types implementation from context-hub#1

@butschster
Copy link
Member

@oddssoft I've created 2.x branch and switched target branch

@oddssoft
Copy link
Author

@butschster

Thanks for the suggestion! I’ll take a look and try to implement union types support based on the context-hub#1 PR. Will keep you updated on the progress.

@oddssoft
Copy link
Author

oddssoft commented Jul 21, 2025

@butschster hi!
This PR is intended for the 1.x version and is compatible with Symfony 6.4–7.3 (even though we are not officially targeting Symfony 7.3 yet).

For version 2.x, I will create a separate PR that refactors the type resolution logic to use getType() instead of the deprecated getTypes() method, which was deprecated in Symfony 7.3.

The goal is to keep the 1.x branch alive and maintainable until Symfony 8.x is released.

@butschster butschster changed the base branch from 2.x to 1.x July 21, 2025 15:32
@butschster butschster merged commit df03892 into spiral:1.x Jul 21, 2025
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants