Notes › Tech Notes › After YAML to JSON, 0644 came back as 420 and 15 of 33 boolean words stayed strings

After YAML to JSON, 0644 came back as 420 and 15 of 33 boolean words stayed strings

A deployment YAML had three perfectly ordinary lines in it: image: 11.10, debug: on and mode: 0644. I dropped the whole file into a converter to get a JSON copy, turned that back into YAML and committed it, and the diff showed three changes nobody had made: 11.10 had lost its trailing zero, on had become true, and 0644 had turned into four hundred and something. The converter is not broken. YAML's implicit type resolution is doing this: it never accepts "this text is this text", it guesses a type for every scalar first and only then decides what to write into JSON.

The traps split into three groups: strings that read as a boolean or as a number, duplicate keys eaten silently, and reference structures such as anchors and multi-document streams. I fed a batch of scalars through the YAML to JSON converter one at a time and wrote down what the input looked like beside what type came out, as one comparison table.

The table: which scalars get replaced

Input as writtenWhat the default tier readsAfter switching tier or quoting
yes / No / ON / offBoolean true / falseStrict tier sends all of them back to strings
y / n / yEs / oFfString, untouchedMixed case is never judged boolean
true / TRUE / FalseBooleanBoth tiers judge boolean
11.10 / 2.0 / 0.5011.1, 2, 0.5Only quoting keeps the trailing zero
11.01 / 1.2.311.01 is a number, 1.2.3 is a stringThree dotted parts are never read as a number
007 / 0x1F / 0o17 / 0b1017, 31, 15, 5Explicit prefixes ignore the tier
08 / 09Whole token falls back to a stringStrict tier reads them as decimal 8, 9
1_000 / .5 / 1e31000, 0.5, 1000Strict tier turns 1_000 into a string
2026-10-23 / 12:30:00 / 2026-13-45All strings, 0 warningsNo Date conversion, no legality check either
.inf / .nanQuoted "Infinity", "NaN", each with 1 warningJSON itself has no such literals
90071992547409939007199254740992Past the safe integer the last digit is rounded away

The boolean word list: 33 spellings, 15 of them are not booleans

The default tier sweeps the whole YAML 1.1 boolean table in one pass: of 33 candidate spellings, 9 read as true, 9 read as false and 15 stay strings. It narrows in two places. Single letters y and n are not booleans, and mixed case spellings such as yEs and oFf are kept verbatim; a value carrying a trailing space such as true is not judged boolean either. Be off by one letter when writing it and the type you read back changes tier.

On a config file the split is easy to see: a: no and b: on come back as "a": false, "b": true, while c: y and f: n are still strings. Going the other way, from JSON to YAML, risky key names get quotes added, so "on": 1 lands as 'on': 1 and the key slot is not swallowed the next time something reads the file back.

Leading zeros are not decimal: 0644 reads as 420

The default tier resolves leading zeros with YAML 1.1 rules: 007 is 7, 017 is 15, 011 is 9, 0644 is 420 and 00 is 0. Permission bits, shard numbers and ticket numbers are exactly the kind of string this rule likes to change. The odd part is that the octal reading only happens when every digit is a legal octal digit: 08 and 09 contain illegal ones, so the whole token falls back to a string.

Explicit prefixes ignore the tier: 0x1F is 31, 0o17 is 15, 0b101 is 5, while a bare 0x or 0b with nothing behind it stays a string. Underscores are simply deleted, so 1_000 becomes 1000 and 1_0.5 becomes 10.5. Nothing announces it; the value you read back is just a different number.

That strict tier switch in the top right corner is not only about booleans. It rewrites the numeric reading in six places too: 011 goes from 9 to 11, 017 from 15 to 17, 08 and 09 from strings to 8 and 9, 1_000 from 1000 back to a string, and 1.5e3 from a string to 1500. What moves is the whole numeric reading, not the yes/no words.

Which fields get hit

The share that breaks is not frightening, but the fields that do share a family resemblance: strings that look like numbers (version numbers, port numbers, permission bits), switch values that look like English words (on, no, off), and dates that do not look like dates. A sexagesimal spelling such as 12:30:00 stays a string in every tier and is never multiplied by 60.

33 boolean spellings swept Reads true 9 words Reads false 9 words Stays a string 15 words

Fig. 1: the 9 / 9 / 15 split from the boolean sweep

Where the precision line is drawn

Floats keep 17 significant digits, so a value such as 0.30000000000000004 is printed as it is and never rounded back to 0.3. The break point is the safe upper bound for integers: past 9007199254740991 precision leaks, 9007199254740993 comes out as 9007199254740992, the last digit is dropped by double precision, and that kind of loss carries no message at all. Quote long IDs and snowflake numbers before converting.

There is no length cap on the input side either: a file of 20000 lines and 397779 bytes finishes in 85 milliseconds and produces 537782 bytes. The reverse direction does not put big integers into scientific notation, so 9007199254740992 written back to YAML is still that same long digit string.

Duplicate keys: the overwrite comes first, the note only in half the cases

Three lines b: 1 / a: 2 / b: 3 come out as "b": 3 and "a": 2: the later value wins, while key order still follows first appearance. A merge does attach a warning, reading "line 3: key b appears twice, the later value overwrote the earlier one". Write the same key across n lines and the warning count is exactly the line count minus one, with the last entry sitting on the line where the overwrite happened.

Two exceptions. A quoted key and a bare key count as the same key, so "k": 1 plus k: 2 merge into one key and give 1 note. Written as a flow collection it behaves differently: in a: {k: 1, k: 2} the overwrite still happens, only "k": 2 is left, and the warning count is 0. Judging from the warning list whether anything was overwritten is not reliable.

Duplicate keys, block mapping 3 lines 2 notes 4 lines 3 notes 5 lines 4 notes Flow repeat in 1 line: 0 notes value still replaced

Fig. 2: duplicate key lines against warning count

Anchors expand completely, merge keys only fill gaps

JSON has no notion of an anchor, so a reference is flattened into the actual value the moment you convert. a: &anchor hello plus b: *anchor gives two copies of "hello"; when an alias points at a sequence the result gets a deep copy instead of a shared reference.

The merge key << has a priority order that is easy to remember backwards: it only fills gaps, it never overwrites. Give the base a: 1 and c: 3, let the child write c: 9 itself, and after expansion a is filled in while c is still 9. When several anchors are listed, the one written last claims its keys first, and the node's own values win over all of them.

s: &s [1, 2]
d:
  <<: *s

Point the merge key at a sequence rather than a mapping and the result is an empty object, with 2 identical warnings reading "line 3: the merge key << does not reference a mapping, skipped". Three failure shapes share one other message: a forward reference (using *a before &a exists), an alias to a name that was never defined, and a circular alias all stop at "references an undefined anchor" and carry full line and column coordinates.

Q: How do I keep a version number a string?

Quoting is what holds: "11.10" and "no" keep their shape across the whole round trip. For the boolean half of the problem you can also tick the strict tier and get strings back, but that tier changes the numeric reading in six places at the same time, which is a bad trade.

Q: Will it flag a Tab mixed into my indentation?

Only a Tab in the indentation position gets stopped, and that error carries line and column and tells you to switch to spaces. A Tab written inside a value or inside a flow collection is treated as whitespace and skipped with no message at all. Mixed with siblings at the same level, what gets reported is still "inconsistent indentation".

Q: Can it convert several documents at once?

Multi-document is the only batch shape: separate them with --- and tick the multi-document switch, and the output becomes a top-level array where an empty document lands as a null element. Without the switch, 2 documents are rejected outright, and that error's coordinates are fixed at line=0 col=0, so the interface shows no line or column information for it.

📎 Related articles

Same 70 Bytes, 13 MIME Types: file-info Reads the Extension, Never the Content 2026-10-23 My Transparent Logo Came Back as a White JPG, and 91 Quality Levels Changed Nothing on the PNG Side 2026-10-23 Remove Everything Still Left 553 Bytes, and Location Only Never Touched the Thumbnail 2026-10-23 Pad Width Is Only a Floor: Where (2) Lands When a Batch Rename Hits 2000 Files 2026-10-23 Two Certificate Fingerprints, 95 Characters and 64: The Missing 31 Are All Colons 2026-10-23