JSON request bodies test values
JSON request bodies that parsers and databases read differently. Rename the example fields to those of your API. 12 values, each with what it breaks and what a correct application does instead.
Type them one at a time with AltShiftN
in the palette, or take them all into a script with nkb emit json-payloads.
Empty request body
json-payloads/empty-body- 0 graphemes
- 0 code points
- 0 bytes
- 0 UTF-16 units
- What it breaks
- An empty body is not JSON; most parsers raise an error and some return no value at all, so a handler written for one of the two answers 500 instead of 400.
- What a correct application does
- 400 with a message that says the body is empty.
Duplicate key
json-payloads/duplicate-key
{"quantity":1,"quantity":100}
{"quantity":1,"quantity":100}
- 29 graphemes
- 29 code points
- 29 bytes
- 29 UTF-16 units
- What it breaks
- RFC 8259 leaves duplicate names undefined: most parsers keep the last value and a few refuse the body, so two layers with different parsers can each see a different quantity.
- What a correct application does
- Rejected with 400, or the same value used by every layer that reads the body.
Integer above 2^53
json-payloads/big-int-id
{"id":9007199254740993}
{"id":9007199254740993}
- 23 graphemes
- 23 code points
- 23 bytes
- 23 UTF-16 units
- What it breaks
- Above 2^53 a double cannot hold every integer: JavaScript and a Go map read 9007199254740993 as 9007199254740992, while a typed int64 keeps it, so an id changes between two services.
- What a correct application does
- Ids above 2^53 - 1 sent as strings, or rejected with a message.
Explicit null
json-payloads/explicit-null
{"name":null}
{"name":null}
- 13 graphemes
- 13 code points
- 13 bytes
- 13 UTF-16 units
- What it breaks
- A typed model reads null and a missing field as the same empty value, so a partial update cannot tell a request to clear the name from one that leaves it alone.
- What a correct application does
- Null and an absent field handled as two different requests, the way the API documents them.
Number sent as a string
json-payloads/number-as-string
{"quantity":"5"}
{"quantity":"5"}
- 16 graphemes
- 16 code points
- 16 bytes
- 16 UTF-16 units
- What it breaks
- Some binders read the string 5 as the number 5 and others refuse it, so the same body passes one service and is rejected by the next one in the chain.
- What a correct application does
- Rejected with 400 naming the field and its type, or converted the same way in every layer.
1001 nested arrays
json-payloads/nesting-1001
[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]
2,002 code points in all, cut in the middle
[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]
- 2,002 graphemes
- 2,002 code points
- 2,002 bytes
- 2,002 UTF-16 units
- What it breaks
- Each parser stops at its own depth, from 64 levels to 1000 or none at all, so a body that one layer parses another rejects with a depth error rather than a syntax error.
- What a correct application does
- Rejected with 400 at a documented depth, before any handler sees it.
Escaped lone surrogate
json-payloads/lone-surrogate
{"name":"\ud800"}
{"name":"\\ud800"}
- 17 graphemes
- 17 code points
- 17 bytes
- 17 UTF-16 units
- What it breaks
- An escaped lone surrogate is valid JSON syntax but not valid text: some parsers refuse it, Go encoding/json replaces it with U+FFFD, and others keep it until writing it as UTF-8 fails or changes it.
- What a correct application does
- Rejected with 400, or replaced visibly before the value is stored.
Escaped NUL in a string
json-payloads/escaped-nul
{"name":"a\u0000b"}
{"name":"a\\u0000b"}
- 19 graphemes
- 19 code points
- 19 bytes
- 19 UTF-16 units
- What it breaks
- JSON parsers keep the NUL inside the string, so it reaches storage: PostgreSQL jsonb refuses it, and code that uses C strings cuts the name after the a.
- What a correct application does
- Rejected with 400 at the API, or the NUL removed before the value is stored.
Number beyond a double
json-payloads/huge-exponent
{"price":1e400}
{"price":1e400}
- 15 graphemes
- 15 code points
- 15 bytes
- 15 UTF-16 units
- What it breaks
- 1e400 is valid JSON and beyond a double: Go and serde refuse it, Python, .NET and Jackson read infinity, JavaScript writes it back as null, and PostgreSQL jsonb keeps all 401 digits.
- What a correct application does
- Rejected with 400 as out of range for the field.
Keys differing only in case
json-payloads/case-variant-keys
{"quantity":1,"Quantity":100}
{"quantity":1,"Quantity":100}
- 29 graphemes
- 29 code points
- 29 bytes
- 29 UTF-16 units
- What it breaks
- Case-insensitive binding, the default in ASP.NET Core and in Go encoding/json, takes the second key, while case-sensitive binders take the first or refuse the unknown one, so layers disagree about the quantity.
- What a correct application does
- Rejected with 400, or one documented key used by every layer.
Trailing comma
json-payloads/trailing-comma
{"quantity":1,}
{"quantity":1,}
- 15 graphemes
- 15 code points
- 15 bytes
- 15 UTF-16 units
- What it breaks
- JavaScript object literals and JSON5 allow a trailing comma and JSON does not, so a body written by hand or by a lenient client fails on a strict server, with 400 or with 500.
- What a correct application does
- 400 with a message that points at the comma.
Byte order mark before the body
json-payloads/bom-before-body
{"quantity":1}
\uFEFF{"quantity":1}
- 1 zero-width character
- 15 graphemes
- 15 code points
- 17 bytes
- 15 UTF-16 units
- What it breaks
- RFC 8259 forbids a byte order mark before JSON and lets parsers ignore one: Jackson and decoders that strip it accept the body, while Go, serde and PostgreSQL refuse it, so one request passes one service and fails another.
- What a correct application does
- Accepted with the mark ignored, or rejected with 400 that names it; never 500.