json1-10 Exercise 2: TOML, JSON5/JSONC, XML and CSV: What Each Parser Accepts and What Each Loses ================================================================================================= Run with Python 3.14.0 (PyYAML 6.0.3 with libyaml, ruamel.yaml 0.19.1, tomllib from the standard library, tomli_w 1.2.0, json5 0.17.3, xmltodict 1.0.4, protobuf 7.36.2 with the upb backend, grpcio-tools 1.84.0) and Node 24.18.0 (yaml 2.9.1, js-yaml 5.4.3, smol-toml 1.9.1, json5 2.2.3, jsonc-parser 3.3.1, fast-xml-parser 5.11.2) on Windows 11, installed in a scratch folder. --- tm.py (TOML in Python; null, duplicate key, nan; XML empty string versus None; an XML attribute) --- import tomllib, json, xmltodict src = '''# a comment title = "demo" when = 2026-10-12T08:30:00Z day = 2026-10-12 mixed = [1, "two", 3.0] hex = 0xFF big = 1_000 [owner] name = "Ada" ''' d = tomllib.loads(src) for k, v in d.items(): print("%-6s %s" % (k, type(v).__name__ + ":" + repr(v))) try: json.dumps(d) except TypeError as e: print("json.dumps ->", e) print("json.dumps(default=str) ->", json.dumps(d, default=str)) for bad in ('a = 1\na = 2\n', 'a = ""\n[a]\nb = 1\n', 'x = nan\n', 'n = null\n'): try: print(repr(bad), "->", tomllib.loads(bad)) except tomllib.TOMLDecodeError as e: print(repr(bad), "-> TOMLDecodeError:", e) print("xml: empty string, None and missing:", [xmltodict.parse(xmltodict.unparse({"r": {"a": v}}))["r"] for v in ("", None)]) print("xml with an attribute:", json.dumps(xmltodict.parse('

hi

'))) Output: title str:'demo' when datetime:datetime.datetime(2026, 10, 12, 8, 30, tzinfo=datetime.timezone.utc) day date:datetime.date(2026, 10, 12) mixed list:[1, 'two', 3.0] hex int:255 big int:1000 owner dict:{'name': 'Ada'} json.dumps -> Object of type datetime is not JSON serializable json.dumps(default=str) -> {"title": "demo", "when": "2026-10-12 08:30:00+00:00", "day": "2026-10-12", "mixed": [1, "two", 3.0], "hex": 255, "big": 1000, "owner": {"name": "Ada"}} 'a = 1\na = 2\n' -> TOMLDecodeError: Cannot overwrite a value (at line 2, column 6) 'a = ""\n[a]\nb = 1\n' -> TOMLDecodeError: Cannot overwrite a value (at line 2, column 3) 'x = nan\n' -> {'x': nan} 'n = null\n' -> TOMLDecodeError: Invalid value (at line 1, column 5) xml: empty string, None and missing: [{'a': None}, {'a': None}] xml with an attribute: {"p": {"@id": "7", "#text": "hi"}} --- nodefmt.mjs (smol-toml and fast-xml-parser in Node) --- import { parse as toml } from "smol-toml"; import { XMLParser } from "fast-xml-parser"; const d = toml('# c\ntitle = "demo"\nwhen = 2026-10-12T08:30:00Z\nday = 2026-10-12\nbig = 1_000\nmixed = [1, "two"]\n'); for (const [k, v] of Object.entries(d)) console.log("smol-toml", k.padEnd(6), v instanceof Date ? "Date:" + v.toISOString() + " (" + v.constructor.name + ")" : typeof v + ":" + JSON.stringify(v)); console.log("JSON.stringify of the parsed TOML:", JSON.stringify(d)); try { toml("n = null\n"); } catch (e) { console.log("smol-toml null ->", e.name); } const x = new XMLParser(); console.log("fast-xml-parser, two servers :", JSON.stringify(x.parse("ab"))); console.log("fast-xml-parser, one server :", JSON.stringify(x.parse("a"))); console.log("fast-xml-parser, 1.10 :", JSON.stringify(x.parse("1.1001234"))); Output: smol-toml title string:"demo" smol-toml when Date:2026-10-12T08:30:00.000Z (TomlDate) smol-toml day Date:2026-10-12 (TomlDate) smol-toml big number:1000 smol-toml mixed object:[1,"two"] JSON.stringify of the parsed TOML: {"title":"demo","when":"2026-10-12T08:30:00.000Z","day":"2026-10-12","big":1000,"mixed":[1,"two"]} smol-toml null -> Error fast-xml-parser, two servers : {"c":{"s":["a","b"]}} fast-xml-parser, one server : {"c":{"s":"a"}} fast-xml-parser, 1.10 : {"c":{"v":1.1,"z":1234}} --- j5.mjs (thirteen inputs through JSON.parse, JSON5 and jsonc-parser; then a JSON5 round trip). Note: the Infinity and NaN cells for JSON5 show null only because the table prints through JSON.stringify; JSON5 returned the real values. --- import JSON5 from "json5"; import { parse as jsoncParse, printParseErrorCode } from "jsonc-parser"; const cases = { "line comment": '{"a":1 // note\n}', "block comment": '{/* c */"a":1}', "trailing comma": '{"a":1,}', "single quotes": "{'a':'x'}", "unquoted key": "{a:1}", "hex number": "{a:0x1F}", "leading plus": "{a:+1}", "leading dot": "{a:.5}", "Infinity": "{a:Infinity}", "NaN": "{a:NaN}", "multi-line string": "{a:'x\\ny'}", "undefined (not allowed)": "{a:undefined}", "ordinary JSON": '{"a":[1,2]}', }; const t = (f, s) => { try { return JSON.stringify(f(s)); } catch (e) { return "ERROR"; } }; const jc = (s, o) => { const errs = []; const v = jsoncParse(s, errs, o); return errs.length ? "errors: " + errs.map((e) => printParseErrorCode(e.error)).join(",") : JSON.stringify(v); }; console.log("input".padEnd(24), "|", "JSON.parse".padEnd(10), "|", "JSON5 2.2.3".padEnd(14), "|", "jsonc-parser default".padEnd(34), "| jsonc allowTrailingComma"); for (const [k, s] of Object.entries(cases)) console.log(k.padEnd(24), "|", t(JSON.parse, s).padEnd(10), "|", t(JSON5.parse, s).padEnd(14), "|", jc(s).slice(0, 34).padEnd(34), "|", jc(s, { allowTrailingComma: true })); console.log("\nJSON5.stringify round trip of a commented file:"); const src = '{\n // port to listen on\n port: 8080,\n tags: ["a",],\n}'; const v = JSON5.parse(src); console.log(JSON5.stringify(v, null, 2)); console.log("JSON5.stringify(NaN) =", JSON5.stringify({ n: NaN }), "| JSON.stringify(NaN) =", JSON.stringify({ n: NaN })); Output: input | JSON.parse | JSON5 2.2.3 | jsonc-parser default | jsonc allowTrailingComma line comment | ERROR | {"a":1} | {"a":1} | {"a":1} block comment | ERROR | {"a":1} | {"a":1} | {"a":1} trailing comma | ERROR | {"a":1} | errors: PropertyNameExpected,Value | {"a":1} single quotes | ERROR | {"a":"x"} | errors: InvalidSymbol,PropertyName | errors: InvalidSymbol,PropertyNameExpected,InvalidSymbol,ValueExpected unquoted key | ERROR | {"a":1} | errors: InvalidSymbol,PropertyName | errors: InvalidSymbol,PropertyNameExpected,ValueExpected hex number | ERROR | {"a":31} | errors: InvalidSymbol,PropertyName | errors: InvalidSymbol,PropertyNameExpected,InvalidSymbol,ValueExpected leading plus | ERROR | {"a":1} | errors: InvalidSymbol,PropertyName | errors: InvalidSymbol,PropertyNameExpected,InvalidSymbol,ValueExpected leading dot | ERROR | {"a":0.5} | errors: InvalidSymbol,PropertyName | errors: InvalidSymbol,PropertyNameExpected,InvalidSymbol,ValueExpected Infinity | ERROR | {"a":null} | errors: InvalidSymbol,PropertyName | errors: InvalidSymbol,PropertyNameExpected,InvalidSymbol,ValueExpected NaN | ERROR | {"a":null} | errors: InvalidSymbol,PropertyName | errors: InvalidSymbol,PropertyNameExpected,InvalidSymbol,ValueExpected multi-line string | ERROR | {"a":"x\ny"} | errors: InvalidSymbol,PropertyName | errors: InvalidSymbol,PropertyNameExpected,InvalidSymbol,ValueExpected undefined (not allowed) | ERROR | ERROR | errors: InvalidSymbol,PropertyName | errors: InvalidSymbol,PropertyNameExpected,InvalidSymbol,ValueExpected ordinary JSON | {"a":[1,2]} | {"a":[1,2]} | {"a":[1,2]} | {"a":[1,2]} JSON5.stringify round trip of a commented file: { port: 8080, tags: [ 'a', ], } JSON5.stringify(NaN) = {n:NaN} | JSON.stringify(NaN) = {"n":null} --- j5.py (the Python json5 package) --- import json5, json for name, s in [("comment", '{"a":1 // c\n}'), ("trailing comma", '{"a":1,}'), ("unquoted key", "{a:1}"), ("Infinity", "{a:Infinity}"), ("ordinary JSON", '{"a":[1,2]}')]: r = [] for f in (json.loads, json5.loads): try: r.append(repr(f(s))) except Exception as e: r.append("ERROR " + type(e).__name__) print("%-16s json.loads: %-22s json5.loads: %s" % (name, r[0], r[1])) print("json5.dumps:", json5.dumps({"a": [1, 2], "n": float("nan")})) Output: comment json.loads: ERROR JSONDecodeError json5.loads: {'a': 1} trailing comma json.loads: ERROR JSONDecodeError json5.loads: {'a': 1} unquoted key json.loads: ERROR JSONDecodeError json5.loads: {'a': 1} Infinity json.loads: ERROR JSONDecodeError json5.loads: {'a': inf} ordinary JSON json.loads: {'a': [1, 2]} json5.loads: {'a': [1, 2]} json5.dumps: {a: [1, 2], n: NaN} --- csvt.py (a record with awkward values through Python's csv module) --- import csv, io, json rows = [{"id": 1, "zip": "01234", "note": 'say "hi", then leave', "multi": "line1\nline2", "n": None, "flag": True, "tags": ["a", "b"], "price": 19.99, "empty": ""}] o = io.StringIO(newline=""); w = csv.DictWriter(o, fieldnames=list(rows[0]), lineterminator="\n"); w.writeheader(); w.writerows(rows) text = o.getvalue(); print(text) back = list(csv.DictReader(io.StringIO(text, newline="")))[0] for k, v in rows[0].items(): b = back[k]; print("%-6s %-26r -> %-26r %s" % (k, v, b, "same" if v == b else "DIFFERENT (%s -> str)" % type(v).__name__)) print("null and empty string both come back as:", repr(back["n"]), repr(back["empty"])) Output: id,zip,note,multi,n,flag,tags,price,empty 1,01234,"say ""hi"", then leave","line1 line2",,True,"['a', 'b']",19.99, id 1 -> '1' DIFFERENT (int -> str) zip '01234' -> '01234' same note 'say "hi", then leave' -> 'say "hi", then leave' same multi 'line1\nline2' -> 'line1\nline2' same n None -> '' DIFFERENT (NoneType -> str) flag True -> 'True' DIFFERENT (bool -> str) tags ['a', 'b'] -> "['a', 'b']" DIFFERENT (list -> str) price 19.99 -> '19.99' DIFFERENT (float -> str) empty '' -> '' same null and empty string both come back as: '' '' WHY THIS WORKS AS AN ANSWER --------------------------- TOML keeps explicit types (a datetime with a UTC offset, a date, hex and underscored integers) and rejects null, a duplicate key and a key redefined as a table, but accepts nan. json.dumps cannot serialise the parsed dates; default=str serialises them with a space instead of a T, which is the trap. JSON5 and jsonc-parser accept different supersets of JSON, and a JSON5 round trip drops the comment and writes unquoted keys and single quotes, so the output is not JSON. In XML, an empty string and a null both come back as None, one repeated element becomes a list and a single one does not, and fast-xml-parser converts 1.10 to 1.1 and 01234 to 1234 by default. In CSV every value comes back as text, null and the empty string merge, Python's csv module writes True and "['a', 'b']" (Python spellings, not JSON), but commas, quotes, newlines and the leading zero in "01234" survive because the module quotes them. Not tested: Excel, other TOML or JSON5 libraries, XML namespaces and attributes beyond one example.