You have a JSON blob from an API and you want it readable. Or you have a YAML config and a tool that only speaks JSON. Converting between the two is a solved problem, but the conversions are not perfectly lossless, and the tooling has sharp corners. Here is the practical playbook: the commands, the libraries, and every trap that will quietly rearrange your data.
Command line: yq, python, jq
yq (the Go one, mikefarah/yq) is the cleanest for ad-hoc work:
yq -o=yaml -I=2 data.json > data.yaml # JSON to YAML
yq -o=json data.yaml > data.json # YAML to JSON
The Python one-liner is everywhere and needs no extra install beyond PyYAML:
python -c "import sys,yaml,json; yaml.safe_dump(json.load(sys.stdin), sys.stdout, default_flow_style=False, sort_keys=False)" < data.json
Note default_flow_style=False - without it, PyYAML may emit nested structures on a single flow line like {a: 1} instead of block style. And sort_keys=False keeps your key order instead of alphabetising it.
jq alone cannot emit YAML, but it is the right tool to reshape JSON before a conversion:
jq '.items[] | {name: .name, price: .price}' data.json | yq -o=yaml
Library code: Python and JavaScript
Python with PyYAML:
import yaml, json
with open("data.json") as f:
data = json.load(f)
with open("data.yaml", "w") as f:
yaml.safe_dump(data, f, default_flow_style=False, sort_keys=False, allow_unicode=True)
In JavaScript, js-yaml is the standard:
const yaml = require("js-yaml");
const fs = require("fs");
const data = JSON.parse(fs.readFileSync("data.json", "utf8"));
fs.writeFileSync("data.yaml", yaml.dump(data, { noRefs: true, lineWidth: -1 }));
The noRefs: true stops js-yaml from inventing anchors for repeated objects (which would make the output invalid to re-parse as plain data in some pipelines), and lineWidth: -1 disables its automatic line wrapping.
Before and after: a real round trip
Input JSON:
{
"name": "checkout",
"enabled": true,
"timeout": 30,
"hosts": ["a", "b"]
}
After yq -o=yaml:
name: checkout
enabled: true
timeout: 30
hosts:
- a
- b
Clean. Now the traps.
Trap 1: long strings get wrapped
By default both PyYAML and js-yaml wrap long strings at around 80 columns. A long URL or base64 blob suddenly sprouts line breaks, which can break parsers that expect one line. Force it off: PyYAML via width=4096 (or lineWidth in js-yaml), and always pass default_flow_style=False for readable block output.
Trap 2: null vs empty
JSON null becomes YAML null (or ~). But an empty string "" and a missing key are different things, and YAML's empty value (a key with nothing after the colon) parses back as null, not "". If your consumer distinguishes "empty string" from "absent", quote empty values explicitly: field: "".
Trap 3: numbers and scientific notation
Large integers and floats can come back in scientific notation. JSON numbers like 1000000000000 may round-trip fine, but a float such as 1e-7 can be re-emitted as 1.0e-07, and a 64-bit integer beyond safe range can lose precision in JavaScript. For IDs and amounts, prefer passing them as strings.
Trap 4: special-character keys
A JSON key like "x-foo" or "http://example" is legal, but in YAML an unquoted key starting with a special character or containing : or # is a syntax error or gets misread. Always quote such keys on output; good libraries do this, but hand-written converters often don't.
Trap 5: yes/no/on/off booleans
YAML parses yes, no, on, off, true, false all as booleans. If a JSON string value happens to be "off", converting to YAML and back can turn it into the boolean false. Quote these strings. This is the single most common silent data corruption in JSON to YAML conversion.
Trap 6: anchors are not generated
JSON has no anchors, so converting JSON to YAML produces none. Going the other way, YAML anchors and aliases collapse into repeated literals - the structure is preserved, the reuse is not. Don't expect a converter to "de-duplicate" your YAML.
Trap 7: comments are always lost
JSON has none, so JSON to YAML just adds none. YAML to JSON always discards comments permanently. There is no converter that round-trips comments, because JSON simply cannot carry them.
Why bother round-tripping at all?
The most useful reason: debugging an API response. You get a 200-line JSON body from a service, paste it into a converter, and suddenly you can read it, add comments, and reason about it. Then convert back to feed a mock. It is a readability bridge, not a storage format.
Big files and memory
Both formats are loaded fully into memory before anything happens. A 2 GB JSON file will not fit in a default Python process comfortably, and streaming YAML is awkward. For large files, stream JSON with ijson or process in chunks; do not expect a one-shot yq to be gentle on RAM.
The fast path
For one-off conversions, the JSON to YAML converter does both directions in the browser, so the data never leaves your machine - handy precisely when the file contains secrets or customer data. After any conversion, skim the booleans and version-looking strings once; that is where silent corruption hides.