Skip to content

[12.x] Add json:unicode cast to support JSON_UNESCAPED_UNICODE encoding #54992

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

Merged
merged 4 commits into from
Mar 13, 2025

Conversation

fuwasegu
Copy link
Contributor

Description

This PR introduces a new json:unicode cast type for Eloquent attributes, allowing JSON encoding with JSON_UNESCAPED_UNICODE. This makes it easier to store and retrieve JSON data without Unicode escaping (\uXXXX), which is particularly useful for applications that handle non-ASCII characters like Japanese, Chinese, or emojis.

Why?

Currently, Eloquent's json cast escapes Unicode characters by default, making JSON-encoded attributes harder to read for humans and increasing the need for manual decoding. Developers often work around this by overriding accessors/mutators or manually using json_encode($value, JSON_UNESCAPED_UNICODE), which is cumbersome.

By introducing json:unicode, developers can simply specify it in the $casts array:

protected $casts = [
    'data' => 'json:unicode',
];

This automatically applies JSON_UNESCAPED_UNICODE when encoding the attribute.

Changes

Core Modifications

  • Updated Illuminate\Database\Eloquent\Casts\Json:
    • Added an optional $flags parameter to Json::encode(), allowing different encoding options.
  • Updated Illuminate\Database\Eloquent\Concerns\HasAttributes:
    • Added json:unicode to the list of castable types.
    • Modified asJson() to check if json:unicode is used and apply JSON_UNESCAPED_UNICODE.

Tests

  • Added testJsonCastingRespectsUnicodeOption()
    • Ensures that json:unicode encodes JSON without escaping Unicode characters.
  • Added testModelJsonUnicodeCastingFailsOnUnencodableData()
    • Verifies that JsonEncodingException is thrown when json:unicode encounters malformed UTF-8 characters.

Before & After Example

Before (using default json cast)

protected $casts = ['data' => 'json'];

$model = new SomeModel();
$model->data = ['こんにちは' => '世界'];
$model->save();

echo $model->toJson();
// {"data":{"\u3053\u3093\u306b\u3061\u306f":"\u4e16\u754c"}}

After (using json:unicode cast)

protected $casts = ['data' => 'json:unicode'];

$model = new SomeModel();
$model->data = ['こんにちは' => '世界'];
$model->save();

echo $model->toJson();
// {"data":{"こんにちは":"世界"}}

Backward Compatibility

  • The default json cast remains unchanged, ensuring full backward compatibility.
  • Developers can opt-in to json:unicode when needed.

Potential Edge Cases Considered

  • Malformed UTF-8 data: Ensured json:unicode still throws JsonEncodingException when encoding fails.
  • Null handling: Verified that null values are properly handled.
  • Attribute retrieval: Ensured both json and json:unicode return arrays when accessed.

* @return string
*/
protected function asJson($value)
protected function asJson($value, $isUnicode = false)
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't like idea of adding a single-use parameter to asJson. What if we wanted to add (for whatever reason), the ability to do JSON_PRETTY_PRINT? Or maybe JSON_PARTIAL_OUTPUT_ON_ERROR?

I think it should be asJson($value, int $flags = 0) and we can add an additional method to process the flags.

Maybe something like this?

protected function getJsonCastFlags($key): int
{
    $flags = 0;

    if ($this->hasCast($key, ['json:unicode'])) {
        $flags |= JSON_UNESCAPED_UNICODE;
    }

    return $flags;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for your review!
I've addressed your feedback in commit 61fad07.
Please let me know if there's anything else that needs adjustment.

Copy link
Contributor

@shaedrich shaedrich left a comment

Choose a reason for hiding this comment

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

You could probably even add a type-check to ./types/Database/Eloquent/Casts to ensure that only a valid integer is returned 🤔

Comment on lines +1350 to +1353
* @param int $flags
* @return string
*/
protected function asJson($value)
protected function asJson($value, $flags = 0)
Copy link
Contributor

Choose a reason for hiding this comment

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

You could possibly make this type more precise:

Suggested change
* @param int $flags
* @return string
*/
protected function asJson($value)
protected function asJson($value, $flags = 0)
* @param int-mask-of<JSON_*> $flags
* @return string
*/
protected function asJson($value, $flags = 0)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It seems like global constants cannot be specified in that way. Also, it's not mentioned in the PHPStan documentation.

https://phpstan.org/writing-php-code/phpdoc-types#integer-masks

image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

image

As expected, it is a parse error.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for the explanation—I didn't know that 😲 👍🏻

@fuwasegu fuwasegu force-pushed the feat/add-json-unicode-cast branch from 9b64895 to 61fad07 Compare March 13, 2025 15:08
@taylorotwell taylorotwell merged commit b7ab1c7 into laravel:12.x Mar 13, 2025
39 checks passed
@fuwasegu fuwasegu deleted the feat/add-json-unicode-cast branch March 13, 2025 15:21
@AndrewMast
Copy link
Contributor

Hey! You forgot to change the other instance of hasCast to getJsonCastFlags. I made a PR to fix it: #55017

@fuwasegu
Copy link
Contributor Author

thank you very much!

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.

4 participants