-
Notifications
You must be signed in to change notification settings - Fork 11.3k
Add JSON serialization consistency for empty arrays to objects #55537
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add JSON serialization consistency for empty arrays to objects #55537
Conversation
…el, Collection, and JsonResource
Wouldn't it make more sense to provide something like JSON Schema to convert PHP data type-safe to JSON? |
@shaedrich thanks for the suggestion! JSON Schema is indeed valuable for validating JSON data against a defined structure, but it serves a somewhat different purpose than what this PR addresses. This PR is focused on the actual serialization behavior within Laravel itself - specifically addressing the inconsistency where the same attribute can serialize to different JSON types (array vs object) depending on whether it's empty or not. This causes runtime issues with frontend clients expecting consistent types. JSON Schema would help validate JSON against a schema after it's generated, but wouldn't change how Laravel itself serializes the data. The issue is that Laravel currently produces inconsistent output (sometimes The approach in this PR gives developers control over the serialization itself, ensuring consistent JSON types without requiring additional validation steps or transformations on the client side. That said, having better JSON Schema support in Laravel would be a valuable addition for different use cases around API validation, but would be complementary rather than a replacement for consistent serialization. |
This is written by an AI, isn't it? Yes, JSON Schema's primary purpose is to validate data against it. But I can't see while its types can't also be used to give a structure for serializing. My problem is that your solution is quite specific and using JSON Schema would help make serialization configurable in general rather than adding a workaround for one very specific problem. |
Thanks for your pull request to Laravel! Unfortunately, I'm going to delay merging this code for now. To preserve our ability to adequately maintain the framework, we need to be very careful regarding the amount of code we include. If applicable, please consider releasing your code as a package so that the community can still take advantage of your contributions! |
@shaedrich thank you for this clarification. Actually, I don't understand exactly how you would implement JSON Schema to resolve this problem. Could you provide an example of code or a concept that illustrates your approach? |
Something like this: $user = new User;
$user->customFields = [];
$user->serialize(schema: <<<'schema'
"customFields": {
"type": "object"
}
schema);
return $user; // customFields now serializes as {} (object) or $user = new User;
$user->customFields = [];
$user->serialize(schema: [
"customFields" => [
"type" => "object",
],
]);
return $user; // customFields now serializes as {} (object) |
@clementbirkle the From a Service Provider's use Illuminate\Database\Eloquent\Casts\Json;
// assuming `MyJsonEncoder` is an invokable class,
// you can use any other callable
Json::encodeUsing(new MyJsonEncoder()); Then, in your custom encoder, you can use whatever heuristics to encode your values. |
Thank you, @shaedrich, for those examples. I think it's definitely a strong solution! However, I don’t like it too much because:
|
Thank you for this interesting trick, @rodrigopedra! Unfortunately, it's specific to the JSON serialization of models and doesn't solve the issue with empty Laravel collections, which sometimes should return an object instead of an array. |
In those cases, you can play with the return collect([])->toJson(\JSON_FORCE_OBJECT); // "{}" Reference: https://www.php.net/manual/en/json.constants.php#constant.json-force-object Just be aware all arrays will be converted to objects, like this: $data = [
'foo' => [1, 2, 3],
];
echo collect($data)->toJson(\JSON_FORCE_OBJECT | \JSON_PRETTY_PRINT); Will output: {
"foo": {
"0": 1,
"1": 2,
"2": 3
}
} |
Thank you @rodrigopedra for this solution. Of course, it will work — but what about this case: class MyModel extends Model
{
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'a_simple_array' => 'array',
'custom_fields' => AsCollection::class,
];
}
}
$model = new MyModel();
$model->toJson(); // {"a_simple_array":[],"custom_fields":[]}
// in this case, we want this output: {"a_simple_array":[],"custom_fields":{}} |
@clementbirkle in that case you can use the custom encoder as I said earlier Json::encodeUsing(new ModelEncoder(['custom_fields']));
$model = new MyModel();
$model->toJson(); The implementation would be similar to the one tried on this PR. You would check the need to encode an empty array as an object on a per-field basis. You can also override the model's public function jsonSerialize(): mixed
{
$data = parent::jsonSerialize();
if (array_key_exists('custom_fields', $data) && blank($data['custom_fields'])) {
$data['custom_fields'] = new \stdClass();
}
return $data;
} You can also create a helper class to reduce any repetition, something like this: class JsonSerializer implements \JsonSerializable
{
public function __construct(
private readonly array $data,
private readonly array $asObject,
) {}
public function jsonSerialize(): array
{
$data = $this->data;
foreach ($this->asObject as $field) {
if (array_key_exists($field, $data) && blank($data[$field])) {
$data[$field] = new \stdClass();
}
}
return $data;
}
} And then: class MyModel extends Model
{
protected $appends = [
'a_simple_array'
'custom_fields',
];
protected function casts(): array
{
return [
'a_simple_array' => 'array',
'custom_fields' => AsCollection::class,
];
}
public function jsonSerialize(): mixed
{
return new JsonSerializer(parent::jsonSerialize(), ['custom_fields']);
}
} |
@rodrigopedra, thank you for those examples. The last one fits best for me. But with my PR, you could do something like this: class MyModel extends Model
{
/**
* The attributes that should always be serialized as JSON objects.
*
* @var list<string>
*/
protected array $serializeAsObjects = [
'custom_fields'
];
protected function casts(): array
{
return [
'a_simple_array' => 'array',
'custom_fields' => AsCollection::class,
];
}
}
// output: {"a_simple_array":[],"custom_fields":{}} Or even better, when it's a custom collection (for more reusability): class MyCustomCollection extends Collection
{
/**
* Determines if empty arrays should be cast to objects when serialized to JSON.
*/
public bool $serializeEmptyAsObject = true;
}
class MyModel extends Model
{
protected function casts(): array
{
return [
'a_simple_array' => 'array',
'custom_fields' => AsCollection::using(MyCustomCollection::class),
];
}
}
// output: {"a_simple_array":[],"custom_fields":{}} What do you think about these examples? Maybe the description of my PR wasn't clear enough with poor examples. |
This PR adds the ability to control how empty arrays are serialized to JSON in Laravel's core classes. The problem is that JSON serialization of empty arrays (
[]
) and associative arrays ({"key":"value"}
) creates inconsistency:[]
{"key":"value"}
This causes issues with frontend clients and type systems where an attribute might change its type depending on whether it's empty or not.
Example 1: Model attributes
With this PR:
Example 2: Empty collection
Why this matters
This is implemented with minimal overhead using a trait that's applied to Model, Collection, and JsonResource to provide consistent behavior across the framework.
Note: No documentation has been added yet, as I'd like to confirm the approach meets the project's expectations before adding it to the docs.