Development

JSON and YAML: the three traps that corrupt data on conversion

Data conversion looks trivial: the API returns JSON, the pipeline wants YAML, the spreadsheet exports CSV. Trivial until a country code "NO" arrives on the other side as the boolean false, a 19-digit id silently changes value, and a version "1.10" becomes the number 1.1. None of these errors shows up: no exception, no red line, the document converts, runs, and only breaks weeks later in production, when nobody remembers it ever went through a converter. This guide opens the three traps that corrupt data silently, the Norway problem, integer precision loss and implicit dates, and kills the myth that JSON and YAML are interchangeable. Every number here was checked against the spec: [RFC 8259](https://www.rfc-editor.org/rfc/rfc8259) for JSON, the [YAML 1.2.2 spec](https://yaml.org/spec/1.2.2/) for YAML, and ECMAScript for the integer limit. The examples use the [data converter](tool:conversor-dados), which does JSON, YAML, CSV and XML in the browser.

J-Kit15 min readIntermediate
  • JSON
  • YAML
  • Serialization
  • Data
  • Development

Key takeaways

  • YAML 1.2 is a superset of JSON, but the old schema (1.1) reads "NO", "yes", "on" and "off" as booleans, the "Norway problem".
  • JavaScript numbers are float64: any id above 9,007,199,254,740,991 (2^53 − 1) loses precision through JSON.parse. Keep large ids as strings.
  • JSON has no date type (it becomes a string), while YAML 1.1 converts dates on its own. Duplicate keys, anchors and an unsafe yaml.load round out the risks.
  • When in doubt, quote ambiguous scalars, check which parsers follow 1.1 or 1.2, and validate the type on both sides of the conversion.

What each format actually guarantees

Before converting, it pays to know each format’s contract, not the vibe, but what the spec promises. JSON is defined by RFC 8259 and ECMA-404: a tiny syntax, no comments, no trailing comma, and text that, when exchanged between open systems, must be encoded in UTF-8. YAML, in the 1.2.2 spec, is explicitly a superset of JSON, aimed at human readability. TOML, at version 1.0.0 (stable since January 2021), was built for obvious, unambiguous configuration. All three describe the same data; they differ in what they assume on your behalf.

JSON (RFC 8259)

  • Strict: mandatory double quotes, no comments, no trailing comma.
  • No date type and no infinite-precision integer defined in the spec.
  • Ideal for machine-to-machine exchange; noisy to hand-edit.

YAML (1.2.2)

  • Superset of JSON, with indentation, comments (#) and anchors.
  • Many implicit scalars: where the "Norway problem" is born.
  • Clean for humans, sensitive to one extra space.

TOML (1.0.0)

  • Explicit types: string, integer, date-time and table with no guessing.
  • No significant indentation; sections in brackets.
  • Built for project configuration files (Cargo, pyproject).

The data converter works with JSON, YAML, CSV and XML (TOML stays out, but it is a useful reference when you want types with no surprises). CSV and XML add their own trap: CSV has no notion of type, every cell is text, and XML mixes elements with attributes, with no single mapping to an object. The table below sums up what each of the converter’s formats delivers.

Formats supported by the data converter and what each one assumes.
FormatTyped?Comments?SpecificationTypical use
JSONYes (limited)NoRFC 8259 / ECMA-404APIs, data exchange
YAMLYes (implicit)Yes (#)YAML 1.2.2Configuration
CSVNo (all text)NoRFC 4180 (de facto)Spreadsheets, tables
XMLNo (text + attributes)Yes (markup)XML 1.0 (W3C)Integrations, documents

JSON and YAML: superset, not synonyms

The YAML 1.2.2 spec says in its introduction that the main focus of version 1.2 was "making YAML a strict superset of JSON" and that it "removed many of the problematic implicit typing recommendations". In plain terms: every valid JSON is valid YAML, but not every YAML is JSON, and the difference in style is large. JSON marks structure with braces, brackets and quotes, ugly to the eye, impossible to mis-indent. YAML uses space indentation (a tab is a syntax error) and lets you drop quotes, which reads clean in configuration at the cost of ambiguous scalars. The same data in both:

// JSON, estrutura explícita / explicit structure
{ "app": "jkit", "port": 3000, "tags": ["web", "tools"] }

# YAML equivalente / equivalent YAML
app: jkit          # comentário permitido / comment allowed
port: 3000
tags:
  - web
  - tools
The same object in JSON and in YAML. The YAML above is itself a valid JSON rewritten.

The word that matters in the spec’s sentence is "strict": the compatibility holds for syntax, not for how each loose value is interpreted. And it is precisely in implicit typing, what an unquoted "NO" or "1.10" becomes, that YAML versions diverge. That is why the spec’s date matters as much as its number. Here is how the standards got here:

  1. 2001JSON is specified

    Douglas Crockford describes the format on json.org, extracted from JavaScript’s object syntax.

  2. 2005YAML 1.1

    The schema with broad implicit typing: "yes", "no", "on", "off" and "NO" resolve to boolean. The origin of the Norway problem.

  3. 2006RFC 4627

    The first JSON RFC registers the format as an internet standard.

  4. 2009YAML 1.2

    Becomes a strict superset of JSON and introduces the core schema, which narrows the boolean to true/false.

  5. 2013ECMA-404 and TOML

    JSON’s syntax is standardized as ECMA-404; the same year TOML is created, focused on configuration.

  6. 2017 and 2021RFC 8259, YAML 1.2.2, TOML 1.0.0

    RFC 8259 (STD 90) consolidates JSON; in 2021 the YAML 1.2.2 revision and TOML’s first stable release ship.

The three traps that corrupt in silence

Most conversion bugs are not a visible syntax error, they are a silent re-interpretation of type. The document converts without complaint; it is the data that changed. Three traps cause most of the pain, and all three depend on which parser and which schema you used.

Trap 1, the Norway problem. In YAML 1.1, the unquoted scalars "NO", "no", "on", "off", "yes", "y" and "n" are booleans. Norway’s country code is NO; unquoted, it resolves to false. PyYAML follows the 1.1 schema by default, so this is the real behavior of millions of files. Run the YAML below through a 1.1 parser and watch the country vanish:

# Entrada YAML (interpretada com o schema 1.1, ex.: PyYAML)
# YAML input (interpreted with the 1.1 schema, e.g. PyYAML)
country: NO
shipping:
  express: yes
  weekend: off

// Saída JSON, os escalares viram booleanos
// JSON output, the scalars become booleans
{
  "country": false,
  "shipping": { "express": true, "weekend": false }
}
The Norway problem: "NO" becomes false, "yes" becomes true, "off" becomes false, all with no error.

YAML 1.2 fixed this: the core schema resolves as boolean only true, True, TRUE, false, False and FALSE. Always confirm which schema your parser uses. This page’s data converter, for instance, runs on the js-yaml library, which in the installed version uses the 1.2 core schema by default, so "country: NO" arrives as the string "NO", not as false. The universal defense, whatever the parser, is one: quote any code, acronym or value that could collide with a boolean.

Trap 2, integer precision. The JSON spec sets no limit on a number’s size, but JavaScript represents every number as a 64-bit floating point (IEEE 754 binary64). The largest integer that fits without loss is Number.MAX_SAFE_INTEGER, defined in the ECMAScript specification. Above it, integers jump by twos, then by fours, and the arithmetic silently rounds.

Number.MAX_SAFE_INTEGER = 2^53 - 1 = 9007199254740991
2^53
a float64 mantissa has 52 explicit bits + 1 implicit, giving 53 bits of integer precision
- 1
the largest integer representable without gaps is 2^53 minus one
The largest integer a JavaScript number represents exactly. From it on, JSON.parse loses precision.
2^53 − 1largest safe integer in JavaScript
9,007,199,254,740,991that limit spelled out (16 digits)
64 bitssize of a Snowflake or int64 id, it does not fit
// Entrada JSON, dois ids acima do limite seguro
// JSON input, two ids above the safe limit
{ "orderId": 9007199254740993, "userId": 1541815603606036480 }

// Depois de JSON.parse e serializar de novo (JavaScript)
// After JSON.parse and re-serializing (JavaScript)
{ "orderId": 9007199254740992, "userId": 1541815603606036500 }
orderId 9007199254740993 becomes ...992 (the odd number does not exist in float64); the 19-digit id slips by 80.

Trap 3, dates. JSON has no date type: a date is always a string, and it is on you to agree on the format (ISO 8601 is the de facto one). YAML 1.1, by contrast, has an implicit timestamp, an unquoted "2026-05-05" is converted to a date object by the parser, changing the value’s type without you asking. Going from YAML to JSON, that object becomes a date string that may not match the format the other side expects. If your field is a version ("2026.05"), a code or a date that needs a timezone, quote it, and when the timezone enters the math, the time zones guide shows why UTC and daylight saving turn a seemingly harmless string into a bug.

Duplicate keys, anchors and the YAML bomb

Beyond the three type traps, a second group of risks lives in the structure and in the parser’s security. Repeated keys, anchors that multiply and an unsafe loader can, respectively, lose data, crash the server and execute code. Each one folds out of the main flow below, open the one you need.

Duplicate keys: allowed by syntax, undefined in effect

RFC 8259 (§4) says "the names within an object SHOULD be unique" and, if they are not, "the behavior of software that receives such an object is unpredictable". Some libraries keep the last pair, some the first, some collect them all, some raise an error. In other words: a JSON with the same key twice is syntactically valid, but which value survives depends on the parser.

YAML is stricter: the spec forbids duplicate mapping keys. Even so, not every parser complains, several just keep the last one. The defense is to validate uniqueness before converting, especially when two config files are merged and a key shows up in both.

Anchors and aliases: reuse with & and *

YAML lets you name a node with an anchor (&name) and reuse it later with an alias (*name), avoiding repetition in large files. It is handy, but it has two consequences: converting to JSON expands the aliases, the reused structure is copied at each point, and the file can balloon. And, like any alias-pointed value, the expansion happens at parse time, not at read time.

Billion laughs: the YAML bomb

Alias reuse becomes a weapon when nested. You define an anchor with a list of ten items, a second anchor with ten copies of the first, a third with ten copies of the second, and so on. Each level multiplies the size by ten; a few lines of text expand to billions of nodes in memory. This is the billion laughs attack (or YAML bomb), a denial of service: the parser tries to materialize the structure and the process is killed for running out of memory.

The mitigation is to cap the expansion: modern parsers count alias resolutions and stop at a ceiling (the npm yaml package limits to 100 by default; SnakeYAML to 128). When accepting YAML from an untrusted source, use a parser with that limit or reject aliases outright.

Why JSON has no comments

The RFC 8259 grammar allows no comment: the only insignificant whitespace is space, tab, line feed and carriage return. A // or /* */ invalidates the whole document. It was a deliberate choice by Douglas Crockford, comments were being stripped by some generators to smuggle in parsing directives, which broke interoperability. If you need a note in a JSON, use a data field (for example "_comment"); if you need a real comment, that is a good sign the right format is YAML or TOML.

Converting in practice, without corrupting

Here is a real CSV-to-JSON conversion. The first CSV line becomes the keys; each following line becomes an object. Since CSV keeps no type, every value arrives as a string, numbers and booleans included, and an empty field becomes "". It is the most obvious case of type loss, and the one that most surprises people who expect 42 to stay a number:

name,age,active
Pedro,42,true
Ana,,false

// vira / becomes:
[
  { "name": "Pedro", "age": "42", "active": "true" },
  { "name": "Ana",   "age": "",   "active": "false" }
]
CSV to JSON: 42 becomes the string "42" and the boolean becomes "true". Coerce the type afterwards.

In the other direction, JSON to CSV, nested structures like a list of roles are serialized as text inside the cell, because a flat table cannot represent hierarchy. Put that together with what we have seen and the risk map looks like this:

The conversion traps, who they hit and how to avoid them.
TrapWho suffersHow to avoid
Accidental boolean (Norway)Parsers on the YAML 1.1 schema (PyYAML)Quote the value; use a 1.2 parser
Integer precision lossJSON.parse and any JavaScript runtimeKeep large ids as strings
Self-converted dateYAML 1.1 implicit timestampQuote it; agree on ISO 8601
Erased typeCSV (everything becomes text)Coerce the type after converting
Duplicate keyJSON (undefined) and lax YAML parsersValidate uniqueness before merging
Code execution / bombUnsafe yaml.load; parser with no alias capsafe_load and an expansion cap

The best way to internalize all of this is to try it. Paste a YAML with an unquoted "NO", a giant id or a loose date into the converter below and watch what comes out the other side, all in the browser, sending nothing to a server:

Try the traps live: JSON, YAML, CSV and XML converted in the browser.Open the tool full page

Locate, convert and validate the type

Often you do not want the whole document, just one value deep inside. A path is that value’s address: starting from the root $, you descend through keys and indexes, $.user.roles[0] grabs the user’s first role. The JSON path finder returns the path of any node you click, which helps you inspect exactly the field you will convert before touching the whole file. Extracting values by a text pattern is the other half of the job, covered by the regex from scratch guide.

  • Confirm which schema your YAML parser uses (1.1 vs 1.2) before trusting any unquoted scalar.
  • Quote codes, acronyms, versions and dates that could collide with a boolean, number or timestamp.
  • Keep ids and monetary values above 2^53 − 1 as strings, from the database to JSON and back.
  • Validate key uniqueness when merging config files.
  • Use safe_load (or equivalent) for YAML from an untrusted source, with an alias-expansion cap.
  • After structuring the data, format the rest of the pipeline, make the query readable in the SQL formatter.

The pattern that solves almost everything is the same in any conversion: locate the value with a path, convert with a parser whose schema you know, and validate the type on the other side. Conversion is never the problem; the assumption that the type survived intact is. If your ids follow a scheme other than integers, the UUID and ULID guide shows alternatives that never come near the float64 limit.

Frequently asked questions

Why does "NO" become false when I convert YAML?
It is the "Norway problem". In the YAML 1.1 schema, the unquoted scalars "NO", "no", "on", "off" and "yes" are booleans, so the country code NO resolves to false. Parsers that follow 1.1 (like PyYAML by default) do this; the YAML 1.2 core schema narrows the boolean to true/false. The universal fix is to quote it: "NO".
Why did my large id change value after converting?
Because JavaScript numbers are 64-bit floating point and lose precision above 9,007,199,254,740,991 (2^53 − 1, Number.MAX_SAFE_INTEGER). An id like 9007199254740993 is rounded to 9007199254740992 when it goes through JSON.parse. The fix is to keep such values as strings from the source.
Does YAML allow comments and JSON does not?
Correct. YAML uses # for comments; the JSON grammar (RFC 8259) allows no comment, so // or /* */ invalidate the whole file. It was a deliberate design decision. If you need a note in a JSON, keep it in a data field (for example "_comment").
Is a duplicate key in JSON an error?
It is not a syntax error, but it is dangerous. RFC 8259 says names should be unique and that, when they are not, the software’s behavior is unpredictable: one parser keeps the last value, another the first, another errors. YAML forbids duplicate keys in the spec, though not every parser complains. Validate uniqueness before converting.
Is PyYAML’s yaml.load safe?
Not on untrusted data. Before 5.1, yaml.load() could execute arbitrary code (CVE-2017-18342) via tags that instantiate Python objects. Use yaml.safe_load(), which builds only simple types. From 5.1 on, load without a Loader warns; FullLoader still had holes, fixed in 5.4. For external input, safe_load, always.
Which format for configuration: JSON, YAML or TOML?
YAML usually wins when humans edit the file, thanks to comments and clean reading, but it demands care with implicit scalars. TOML shines in project configuration for its explicit types (date-time included) and zero ambiguity. JSON is better when the config is generated and consumed by machines, being strict. Choose by the editor: a human leans YAML/TOML, a machine leans JSON.

JSON and YAML describe the same data with different philosophies, and conversion is safe only when you respect what each one assumes. The three silent traps are the Norway problem (a "NO" that turns into false in the YAML 1.1 schema), integer precision loss above 2^53 − 1 = 9,007,199,254,740,991, and dates that YAML 1.1 converts on its own. Add duplicate keys, anchors that blow up into billion laughs, and the unsafe yaml.load. The defense fits in one sentence: quote ambiguous scalars, keep large ids as strings, check your parser’s schema, and validate the type on both sides.

Sources & references

  1. RFC 8259, The JavaScript Object Notation (JSON) Data Interchange Format
  2. ECMA-404, The JSON Data Interchange Syntax
  3. YAML 1.2.2, Official specification (includes the core schema, §10.3)
  4. ECMAScript (ECMA-262), Number.MAX_SAFE_INTEGER
  5. PyYAML, official guidance on yaml.load and safe_load
  6. TOML v1.0.0, Specification
  7. js-yaml, YAML parser used by the converter (default schema)