Skip to content
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

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

Open
wants to merge 3 commits into
base: 12.x
Choose a base branch
from

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)

* Get JSON casting flags for the specified attribute.
*
* @param string $key
* @return int
Copy link
Contributor

Choose a reason for hiding this comment

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

Therefore, you can narrow the return here as well:

Suggested change
* @return int
* @return int-mask-of<JSON_*>

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.

3 participants