{
  "suite": "canonical",
  "description": "RFC 8785 canonical JSON. `input` is the value; `expect.canonical` is the exact text a conforming implementation must emit, and `errorCases` are values it must REFUSE rather than coerce. These are the bytes every digest and every signature on the network is taken over, so an implementation that disagrees here disagrees about everything downstream.",
  "cases": [
    {
      "id": "integer-like-keys-sort-by-code-unit",
      "note": "THE trap, and no naive implementation gets it right. JavaScript objects do not preserve insertion order for integer-like keys: an object with keys \"10\" and \"9\" enumerates as 9 then 10, because the engine hoists array-index-like keys and orders them numerically. RFC 8785 requires UTF-16 code-unit order, where \"10\" precedes \"9\". Sorting keys into an object and calling a stringifier silently emits the wrong order, and the same trap applies in reverse to tests, because parsing the output back re-hoists.",
      "input": {
        "10": "a",
        "9": "b",
        "1": "c"
      },
      "expect": {
        "canonical": "{\"1\":\"c\",\"10\":\"a\",\"9\":\"b\"}"
      }
    },
    {
      "id": "keys-sort-by-code-unit-not-locale",
      "note": "Never a locale-aware comparison: two machines would produce different bytes for the same record and every signature would fail to verify across them.",
      "input": {
        "b": 1,
        "A": 2,
        "a": 3,
        "B": 4
      },
      "expect": {
        "canonical": "{\"A\":2,\"B\":4,\"a\":3,\"b\":1}"
      }
    },
    {
      "id": "nested-objects-sort-at-every-level",
      "input": {
        "z": {
          "b": 1,
          "a": 2
        },
        "a": [
          {
            "d": 1,
            "c": 2
          }
        ]
      },
      "expect": {
        "canonical": "{\"a\":[{\"c\":2,\"d\":1}],\"z\":{\"a\":2,\"b\":1}}"
      }
    },
    {
      "id": "array-order-is-preserved",
      "note": "Arrays are sequences. Sorting one would change what the document says.",
      "input": {
        "xs": [3, 1, 2]
      },
      "expect": {
        "canonical": "{\"xs\":[3,1,2]}"
      }
    },
    {
      "id": "numbers-use-ecmascript-number-to-string",
      "note": "Including negative zero, which serialises as 0.",
      "input": {
        "a": 1,
        "b": 1.5,
        "c": -0.0,
        "d": 1e21,
        "e": 1e-7
      },
      "expect": {
        "canonical": "{\"a\":1,\"b\":1.5,\"c\":0,\"d\":1e+21,\"e\":1e-7}"
      }
    },
    {
      "id": "strings-use-shortest-form-escaping",
      "input": {
        "a": "quote\" backslash\\ newline\n tab\t"
      },
      "expect": {
        "canonical": "{\"a\":\"quote\\\" backslash\\\\ newline\\n tab\\t\"}"
      }
    },
    {
      "id": "non-ascii-is-not-escaped",
      "note": "The RFC delegates to the ECMAScript serialiser, which emits a \\u escape only where required.",
      "input": {
        "de": "Pufferüberlauf",
        "ja": "脆弱性",
        "emoji": "🔒"
      },
      "expect": {
        "canonical": "{\"de\":\"Pufferüberlauf\",\"emoji\":\"🔒\",\"ja\":\"脆弱性\"}"
      }
    },
    {
      "id": "control-characters-are-escaped",
      "input": {
        "a": "\u0000\u001f"
      },
      "expect": {
        "canonical": "{\"a\":\"\\u0000\\u001f\"}"
      }
    },
    {
      "id": "empty-containers",
      "input": {
        "o": {},
        "a": []
      },
      "expect": {
        "canonical": "{\"a\":[],\"o\":{}}"
      }
    },
    {
      "id": "null-is-a-value-and-is-kept",
      "note": "Distinct from an absent property: null is something the author wrote.",
      "input": {
        "a": null
      },
      "expect": {
        "canonical": "{\"a\":null}"
      }
    },
    {
      "id": "top-level-scalar",
      "input": "hello",
      "expect": {
        "canonical": "\"hello\""
      }
    }
  ],
  "errorCases": [
    {
      "id": "refuse-non-finite-number",
      "note": "A stringifier that emits null here would sign a value nobody wrote. Written as a literal because JSON cannot carry Infinity.",
      "inputLiteral": "{\"a\": Infinity}",
      "expect": {
        "error": "non-finite"
      }
    },
    {
      "id": "refuse-undefined-inside-an-array",
      "note": "It would become null and silently change the length-and-position meaning of a list a signature is about to cover. As an object PROPERTY it is dropped instead, because that is how an absent optional field arrives.",
      "inputLiteral": "{\"a\": [1, undefined, 2]}",
      "expect": {
        "error": "undefined-in-array"
      }
    },
    {
      "id": "refuse-a-non-plain-object",
      "note": "A Date, Map or class instance would either lose data or serialise through toJSON, and a signature over a lossy projection of the caller's value is worse than a refusal.",
      "inputLiteral": "{\"a\": new Date(0)}",
      "expect": {
        "error": "non-plain-object"
      }
    }
  ]
}
