Skip to content
datahacks.dev
YAML

Converting YAML to JSON in Python Without Date Errors

Convert YAML to JSON with PyYAML and json, and handle the cases that break it: dates, sets, integer keys, NaN, anchors, several documents and complex keys.

6 min read
Summarize in AI
Flat illustration of a tree of nested objects beside the document it came from, with the YAML logo.

Converting YAML to JSON feels like a one-liner, and for a plain configuration file it is. The trouble starts when the YAML contains something JSON has no word for. YAML has dates, sets, keys that are not strings, anchors and several documents in one file, and JSON has none of them. This post shows the conversion in Python, then walks through each value that fails or changes and what to do about it. It belongs to the cluster on converting YAML to and from JSON, CSV, XML and Excel, and the command-line version is in converting YAML to JSON with yq.

The basic conversion

Two calls do it:

Python
import json
import yaml

data = yaml.safe_load(open("config.yaml"))
print(json.dumps(data, indent=2))

Use safe_load, never load without a loader. The unsafe loader can construct arbitrary Python objects from a YAML tag, which is why loading untrusted YAML can run code. json.dumps accepts the result when every value is a string, number, boolean, null, list or dictionary with string keys. Anything else is a case below.

A date is not JSON

YAML 1.1, which PyYAML implements, resolves an unquoted 2026-10-27 to a date and 2026-10-27 08:00:00 to a datetime. Given this document:

YAML
name: orders
released: 2026-10-27
at: 2026-10-27 08:00:00
ratio: .nan
ids: !!set {a, b}
1: one
tags: [a, b]

yaml.safe_load returned:

Text
{'name': 'orders', 'released': datetime.date(2026, 10, 27), 'at': datetime.datetime(2026, 10, 27, 8, 0), 'ratio': nan, 'ids': {'b', 'a'}, 1: 'one', 'tags': ['a', 'b']}

and json.dumps of it failed:

Text
TypeError: Object of type date is not JSON serializable

The standard fix is the default hook, which json.dumps calls for any object it cannot handle:

Python
json.dumps(data, default=str)

With default=str, the date became "2026-10-27", the datetime "2026-10-27 08:00:00" and the set the string "{'b', 'a'}". That last result shows the weakness: str is a catch-all, and for a set it produces Python's repr, which is not what a consumer wants. Write a small function instead:

Python
import datetime

def encode(o):
    if isinstance(o, (datetime.date, datetime.datetime)):
        return o.isoformat()
    if isinstance(o, set):
        return sorted(o)
    raise TypeError(f"cannot encode {type(o).__name__}")

isoformat() gives 2026-10-27 and 2026-10-27T08:00:00, the forms most JSON consumers expect, and sorted turns a set into a list in a stable order. Raising on everything else keeps the conversion honest: an unknown type should stop you, not slip through as a string. If the dates were meant to be text, quote them in the YAML source; YAML strings, quoting and escapes explains why.

Keys become strings

JSON object keys are always strings. The YAML key 1: one is an integer key in Python, and json.dumps converted it:

Text
{"name": "orders", "1": "one", "tags": ["a", "b"]}

The key is now "1". Loading that JSON back into Python gives a string key, so a round trip does not return the original. For configuration this rarely matters. For a mapping keyed by numbers, such as HTTP status codes, decide whether your consumer wants strings, and convert on purpose.

A harder case is a key that is not a string or a number at all. YAML allows a complex key introduced with ?, for example a list. PyYAML refuses it when constructing the dictionary:

Text
ConstructorError while constructing a mapping
    ? [1, 2]
found unhashable key

There is no JSON equivalent, so such a document cannot be converted without redesigning it.

NaN and infinity

YAML spells not-a-number .nan and infinity .inf. Python floats can hold them, and json.dumps writes them as NaN and Infinity, which are not valid JSON:

Text
{"ratio": NaN}

Many parsers, including browsers' JSON.parse, reject that text. Pass allow_nan=False so the conversion fails when it meets one:

Text
ValueError: Out of range float values are not JSON compliant: nan

Then decide what the value means. A missing measurement should probably be null. Replace non-finite floats before dumping, or fix the source.

Anchors become copies

Anchors, aliases and merge keys are YAML's way to share a definition. The loader resolves them, so the JSON has no memory of the sharing:

YAML
base: &b {x: 1, y: 2}
use: *b
more:
  <<: *b
  y: 3
Text
{"base": {"x": 1, "y": 2}, "use": {"x": 1, "y": 2}, "more": {"x": 1, "y": 3}}

Three objects, each complete. The conversion is correct and loses the structure that made the YAML maintainable, so JSON generated from YAML is a build artifact: edit the YAML, regenerate the JSON, and never hand-edit the output. The mechanics of merge keys are in YAML anchors, aliases and merge keys.

Several documents

A YAML file can hold many documents separated by ---. safe_load raises an error on such a file, and safe_load_all returns a generator:

Python
docs = list(yaml.safe_load_all("a: 1\n---\nb: 2\n"))
print(json.dumps(docs))
Text
[{"a": 1}, {"b": 2}]

JSON has no multi-document form, so you choose: an array of documents, as above, or newline-delimited JSON with one document per line, which streams better for large inputs. Kubernetes manifests and Compose overrides arrive in this shape. Loading multiple documents from one YAML file covers the loader.

A complete converter

Put the pieces together:

yaml2json.pyPython
import datetime
import json
import sys

import yaml


def encode(o):
    if isinstance(o, (datetime.date, datetime.datetime)):
        return o.isoformat()
    if isinstance(o, set):
        return sorted(o)
    raise TypeError(f"cannot encode {type(o).__name__}")


docs = list(yaml.safe_load_all(sys.stdin))
out = docs[0] if len(docs) == 1 else docs
json.dump(out, sys.stdout, default=encode, allow_nan=False, indent=2, ensure_ascii=False)
print()

It reads standard input, so it composes in a pipeline: python yaml2json.py < config.yaml > config.json. One document produces an object and several produce an array. Dates and sets are handled, NaN is an error, and ensure_ascii=False keeps accented characters readable rather than writing é. For checking the output, the YAML to JSON converter in the browser shows the same conversion without installing anything.

Check the YAML 1.1 surprises before converting

The loader decides types before JSON ever sees the data, and PyYAML follows YAML 1.1. An unquoted no becomes false, 010 becomes 8, and 3.10 becomes 3.1. The JSON is a faithful copy of a wrong value. If the YAML came from a person, scan the converted output for values you did not expect, or load with ruamel.yaml for YAML 1.2 behavior. The differences are laid out in YAML 1.1 versus 1.2.

When a YAML to JSON conversion cannot be lossless

Comments. JSON has no comments, so they are dropped. If the comments carry meaning, keep the YAML as the source and generate the JSON.

Anchors and sharing. The output duplicates shared content. Fine for a consumer, a loss for an editor.

Tags and custom types. A YAML tag such as !!binary or a custom !Ref from CloudFormation has no JSON counterpart. The loader needs a constructor for each, and the output needs a convention.

Key order and types. JSON objects are unordered by specification, though Python keeps insertion order, and numeric keys become strings.

Big numbers. A YAML integer larger than 2^53 survives in Python and may not in a JavaScript consumer, which stores numbers as doubles. Quote identifiers that are long digit strings.

The json module documents default, allow_nan and ensure_ascii in the Python standard library reference, and the PyYAML loaders in the PyYAML documentation. The reverse direction, with its own type traps, is converting JSON to YAML without losing types.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.