Every JSON serializer escapes differently
RFC 8259 §7 requires exactly three things to be escaped inside a string: the quotation mark, the reverse solidus, and U+0000–U+001F. Everything beyond that is a serializer’s choice — and 14 of them make different ones.
Same input, twelve serializers
| é (U+00E9) | 😀 (U+1F600, astral) | < > & (HTML-significant) | ' (apostrophe) | / (solidus) | U+2028 LINE SEPARATOR | |
|---|---|---|---|---|---|---|
| JavaScript · V8 | é | 😀 | <a>&b | it's | a/b |
|
| JavaScript · JavaScriptCore | é | 😀 | <a>&b | it's | a/b |
|
| Python · default | \u00e9 | \ud83d\ude00 | <a>&b | it's | a/b | \u2028 |
| Python · ensure_ascii=False | é | 😀 | <a>&b | it's | a/b |
|
| Go · encoding/json v1 | é | 😀 | \u003ca\u003e\u0026b | it's | a/b | \u2028 |
| Go · SetEscapeHTML(false) | é | 😀 | <a>&b | it's | a/b | \u2028 |
| Java · Jackson | é | 😀 | <a>&b | it's | a/b |
|
| Java · Gson | é | 😀 | \u003ca\u003e\u0026b | it\u0027s | a/b | \u2028 |
| .NET · System.Text.Json | \u00E9 | \uD83D\uDE00 | \u003Ca\u003E\u0026b | it\u0027s | a/b | \u2028 |
| .NET · Newtonsoft | é | 😀 | <a>&b | it's | a/b | \u2028 |
| PHP · json_encode | \u00e9 | \ud83d\ude00 | <a>&b | it's | a\/b | \u2028 |
| Ruby · JSON.generate | é | 😀 | <a>&b | it's | a/b |
|
Amber means the character was changed. Node.js 22, CPython 3.12, Go 1.22, Java 21 (Jackson 2.14 / Gson 2.10), .NET 8, PHP 8.3, Ruby 3.2, JavaScriptCore via Bun 1.4.
The ones worth knowing
Go escapes <, > and &. json.Marshal emits \u003c, \u003e and \u0026 so a browser cannot reinterpret embedded JSON as HTML. It surprises everyone once. Use an Encoder with SetEscapeHTML(false). Go’s encoding/json/v2 drops the default in favour of minimal encoding.
Python and PHP escape every non-ASCII character. json.dumps() defaults to ensure_ascii=True, so café ships as caf\u00e9. Both are valid and represent the same string; they are just not byte-identical, which matters if you are diffing or hashing output.
Gson and System.Text.Json escape the apostrophe. Both turn ’ into \u0027. Jackson does not. If Gson and Jackson output differ byte-for-byte on the same object, this is usually the reason.
U+2028, legal in JSON and illegal in JavaScript
RFC 8259 §12 notes that U+2028 LINE SEPARATOR and U+2029 PARAGRAPH SEPARATOR are valid in JSON but not in JavaScript. That is why the split in the table falls where it does: Go, .NET, PHP and Newtonsoft escape them; the JavaScript engines and Jackson do not.
If you embed JSON in a <script> block without escaping them, a string containing U+2028 terminates the statement and breaks the page. Modern JavaScript engines accept these characters in string literals since ES2019, but the JSON you emit may be consumed by something older.
Practical rules
Never compare serializer output byte-for-byte across languages. Compare parsed values, or canonicalize both sides first.
Escaping non-ASCII is a choice, not a requirement. RFC 8259 §8.1 requires UTF-8 on the wire, so café is perfectly valid unescaped. Escape it when a downstream system is ASCII-only, not by reflex.
Our escape tool reproduces each of these defaults exactly, so you can generate output that matches a specific runtime rather than guessing.
Standards referenced
RFC 8259 §7 fixes the required escapes; everything else in the table is optional behaviour.
- RFC 8259 (STD 90) §7 — Strings — The JavaScript Object Notation (JSON) Data Interchange Format, T. Bray, Ed., 2017.
- RFC 8259 (STD 90) §8.1 — Character Encoding — The JavaScript Object Notation (JSON) Data Interchange Format, T. Bray, Ed., 2017.
- RFC 8259 (STD 90) §12 — Security Considerations — The JavaScript Object Notation (JSON) Data Interchange Format, T. Bray, Ed., 2017.
- ECMA-404, 2nd edition §strings — 9 Strings — The JSON Data Interchange Syntax, Ecma International, 2017.
Only standards bodies and peer-reviewed venues are cited here.
How this was measured
Each serializer was executed on the same set of inputs and its output recorded verbatim, 140 observations in total. Where a language offers a relaxed mode it is shown as a separate row rather than folded into the default.
Every figure on this page came from executing the parser named, not from documentation. The collection harness is public — see the error registry method notes.