In a Laravel Blade view, how should you hand a PHP array or string to an inline script, and what goes wrong with {{ json_encode($data) }}?
answer
- script content is never entity-decoded
- Js::from returns an Htmlable
- arrays become JSON.parse('...')
- JSON_HEX_TAG, APOS, AMP, QUOT
- @js directive; @json splits on commas
basics
~10 sUse Js::from($data) or the @js directive: they emit a JavaScript expression with <, >, &, and quotes hex-escaped. {{ json_encode() }} fails because e() turns quotes into ", which a script block never decodes.
solid answer
~40 s`{{ json_encode($data) }}` runs the JSON through `e()`, so every `"` becomes `"`; inside a `<script>` element the browser does not decode entities, and the JavaScript breaks. A raw `{!! json_encode($data) !!}` is fragile instead: it survives in a script block mainly because PHP escapes `/` by default, and it is unsafe in an attribute. The Laravel tool is `Js::from($data)`, or the `@js($data)` directive that compiles to it. It encodes with `JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT`, returns a single-quoted string literal for strings and a `JSON.parse('...')` call for arrays and objects, and because `Js` is `Htmlable`, `{{ Js::from($data) }}` is not escaped twice. Since Laravel 13 it also adds `JSON_UNESCAPED_UNICODE`.
code
html · 8 lines{{-- $forumConfig built in the controller: ['userId' => 7, 'nick' => "O'Brien </script>"] --}}
<script>
window.forumConfig = {{ Js::from($forumConfig) }};
window.greeting = @js($greeting);
</script>
{{-- Broken: quotes arrive as " inside the script --}}
{{-- <script>var cfg = {{ json_encode($forumConfig) }};</script> --}}go deeper
Recall that data for a script goes through Js::from or @js, never through a plain {{ json_encode() }}.
Explain why script blocks do not decode entities, what the four JSON_HEX flags neutralise, and why Js being Htmlable avoids double escaping.
Pick the right tool per position - script block, JavaScript attribute, data attribute - and keep payloads minimal so models do not leak fields to the page.
Set a team convention that PHP-to-JavaScript data crosses through one helper, with minimal payloads and a review rule against raw json_encode in views.
## Why a script block is a different position An inline `<script>` element is a **raw text element**: the browser's HTML parser does not decode entities inside it, and it ends the element at the first `</script` it meets. That breaks both obvious approaches: - `{{ json_encode($data) }}` compiles to `e(json_encode($data))`. `e()` HTML-escapes the JSON, so `{"name":"Ana"}` becomes `{"name":"Ana"}` - and because nothing decodes it, the JavaScript engine sees `"` and throws a syntax error. - `{!! json_encode($data) !!}` produces valid JSON, but its safety is accidental. PHP's `json_encode` escapes `/` as `\/` by default, which is what stops a value containing `</script>` from closing the element. It does not escape `<`, `>`, `&` or quotes, so the same output placed in an HTML attribute can break out, and a teammate adding `JSON_UNESCAPED_SLASHES` for tidier output reopens the script-block hole. ## Js::from and the @js directive `Illuminate\Support\Js::from($data, $flags = 0, $depth = 512)` builds a JavaScript expression meant to be dropped straight into a template. Its behaviour, read from `Support/Js.php`: 1. **Required flags** are always added: `JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR`. The four `HEX` flags turn `<`, `>`, `'`, `&` and `"` into `\u003C`-style escapes, so the output contains no character that can end a script element or a quoted attribute. 2. **Strings** become a single-quoted JavaScript string literal. 3. **Arrays and objects** become `JSON.parse('...')` over the escaped JSON text (an empty `[]` or `{}` is emitted as-is); numbers, booleans and `null` are emitted as literals. 4. `Arrayable` values are converted with `toArray()`, `Jsonable` values with `toJson()`, enums with their value. 5. Encoding errors throw `JsonException` rather than silently printing `false`. `Js` implements `Htmlable`, so `{{ Js::from($config) }}` is **not** HTML-escaped a second time - `e()` returns its `toHtml()` as-is. Laravel's default class aliases include `Js`, so the short name works in any Blade view without an import. `@js($config)` is the directive form and compiles to `<?php echo \Illuminate\Support\Js::from($config)->toHtml() ?>`. ## The older @json directive Blade still compiles `@json($data)` into `json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT, 512)`, echoed raw. Two cautions: - The directive finds its optional flags and depth by **splitting the argument on commas**, so an inline array literal with several elements is mis-parsed: flags can be silently dropped or the compiled PHP can fail. Pass a variable. - The 13.x Blade docs describe `Js::from` for this job and no longer document `@json`; new code should prefer `Js::from` or `@js`. ## Choosing by position | Where the data lands | Use | Why | |---|---|---| | `<script>` block, variable initialiser | `{{ Js::from($x) }}` or `@js($x)` | hex-escaped expression, no double escaping | | Quoted attribute read as JavaScript (`x-data`, `onclick`) | `Js::from` / `@js` | output contains no raw quotes | | Quoted attribute read as data (`data-config`) | `{{ json_encode($x) }}` | `e()` is right here: the browser decodes entities before `dataset` returns the value | | Element text | `{{ $x }}` | ordinary HTML escaping | ## A worked example For a forum thread page that needs the current member id and nickname in a script: 1. The controller builds a small array: `['memberId' => $member->id, 'nick' => $member->nick]`. 2. The view prints `window.forumConfig = {{ Js::from($forumConfig) }};`. 3. A nickname of `O'Brien </script>` is emitted with the quote as `'` and the angle brackets as `<` and `>` inside the escaped JSON text, so the script element stays closed and the string stays intact. 4. In the browser, `JSON.parse` turns the text back into an object whose `nick` is exactly `O'Brien </script>`, now plain data that frontend code should still insert with `textContent`, not `innerHTML`. ## Practical cautions - Pass **only the fields the script needs**. Handing an Eloquent model to `Js::from` serialises every visible attribute, which is easy to over-share. - Keep the directive argument simple; the Laravel docs warn that complex expressions inside it may fail because Blade parses templates with regular expressions. - In Laravel 13, non-ASCII characters are emitted as-is instead of `\u00e8`-style escapes. That changes the bytes but not the value a browser reads; only snapshot tests comparing raw output notice.
- Why is {{ Js::from($x) }} not escaped twice, when {{ }} always calls e()?`Js` implements `Htmlable`, and `e()` returns an `Htmlable`'s `toHtml()` without calling `htmlspecialchars`. The escaping for the script position already happened inside `Js::from` through the JSON hex flags, so the curly form passes the expression through untouched.
- When is {{ json_encode($x) }} actually correct?Inside a quoted attribute read as data, such as `data-config`. The browser decodes the entities when it parses the attribute, so `element.dataset.config` returns the original JSON text for `JSON.parse`. The same expression is wrong inside a script block, where nothing decodes the entities.
- What does @json(['a' => 1, 'b' => 2, 'c' => 3]) risk?The directive splits its argument on commas to find optional flags and depth, so the array literal is cut into pieces. The pieces happen to reassemble into a call without the hex flags, or with more elements into PHP that fails to compile. Assign the array to a variable and pass that.
saying these in an interview costs you the question
- {{ json_encode($data) }} is the safe way to put data in a script
- Js::from output gets HTML-escaped again inside {{ }}
- {!! json_encode() !!} is safe in any position because it is valid JSON
- @json accepts any PHP expression, including inline array literals
- Escaping only matters for strings, so arrays can be echoed raw