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.

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:
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:
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:
{'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:
TypeError: Object of type date is not JSON serializableThe standard fix is the default hook, which json.dumps calls for any object it cannot handle:
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:
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:
{"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:
ConstructorError while constructing a mapping
? [1, 2]
found unhashable keyThere 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:
{"ratio": NaN}Many parsers, including browsers' JSON.parse, reject that text. Pass allow_nan=False so the conversion fails when it meets one:
ValueError: Out of range float values are not JSON compliant: nanThen 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:
base: &b {x: 1, y: 2}
use: *b
more:
<<: *b
y: 3{"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:
docs = list(yaml.safe_load_all("a: 1\n---\nb: 2\n"))
print(json.dumps(docs))[{"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:
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.


