Formatter Validator Minifier Escape / Unescape JSON ↔ String JSON ↔ YAML JSON → CSV NDJSON / JSON Lines JSON → Types JSON ↔ XML JSON Diff JSON Patch Canonicalize (RFC 8785) Schema Validator Schema Generator JSONPath Tester JWT Decoder JSON Errors Schemas More Tools
JSONTools / Blog / Measured
Measured

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>&bit'sa/b
JavaScript · JavaScriptCoreé😀<a>&bit'sa/b
Python · default\u00e9\ud83d\ude00<a>&bit'sa/b\u2028
Python · ensure_ascii=Falseé😀<a>&bit'sa/b
Go · encoding/json v1é😀\u003ca\u003e\u0026bit'sa/b\u2028
Go · SetEscapeHTML(false)é😀<a>&bit'sa/b\u2028
Java · Jacksoné😀<a>&bit'sa/b
Java · Gsoné😀\u003ca\u003e\u0026bit\u0027sa/b\u2028
.NET · System.Text.Json\u00E9\uD83D\uDE00\u003Ca\u003E\u0026bit\u0027sa/b\u2028
.NET · Newtonsofté😀<a>&bit'sa/b\u2028
PHP · json_encode\u00e9\ud83d\ude00<a>&bit'sa\/b\u2028
Ruby · JSON.generateé😀<a>&bit'sa/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.

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.

More on this