TOML to JSON Schema
Turn a working config into a schema that validates the next one. Every table becomes an object with typed properties, arrays get an items schema merged across their elements, and datetimes come out as strings with format: date-time.
What this tool does
It reads one TOML document as an example and writes the JSON Schema that would accept it. Tables become type: object with a properties map; scalars become string, integer, number, or boolean; arrays become type: array with an items schema derived from their elements. Datetimes become strings with format: date-time, because JSON has no date type.
The intent it closes: "I want CI to reject a malformed config, and I do not want to hand-write the schema." Since TOML and JSON share a data model, a JSON Schema validator is a perfectly good TOML validator once the file is parsed — which is how most editors provide autocomplete for pyproject.toml today.
For an array of tables, the item schemas are merged rather than taken from the first element: properties are unioned, and required is the intersection — so a field that only some entries carry is allowed but not demanded.
When you'd reach for it
- Gate a config in CI. Validate every commit's config against the schema and fail fast on a typo.
- Get editor autocomplete. Point your editor's schema mapping at the generated file and typing a key suggests the rest.
- Document the shape. A schema is a precise, machine-checkable description that does not drift the way prose does.
- Bootstrap a hand-written schema. Generate the structure, then add the constraints — ranges, patterns, enums — by hand.
- Compare two configs structurally. Generate a schema from each and diff the schemas rather than the values.
How it works
One pass over the example document, with the merging rule doing the interesting work.
1. Parse the example
The TOML is parsed into a value tree. Because TOML's types are richer than JSON's, two mappings are decided here: datetimes become strings with a format, and integers are distinguished from floats so you get integer rather than number where that is true.
2. Describe each value
Scalars become a one-line schema with their type — plus an examples array if you asked for it. Tables become objects: a properties entry per key, required listing every key present (unless you turned that off), and additionalProperties set only if you chose to pin it.
3. Merge array items
An array's elements are each described, then merged. If they are all objects, the merged schema unions their properties and keeps only the keys that appear in every element as required. If they are all the same scalar type, that type is used. Otherwise the result is an anyOf, which is honest about a genuinely heterogeneous array.
Options reference
Draft
2020-12 is current and what most validators default to. draft-07 is still the most widely supported in older tooling — pick it if your validator or editor is specific about it. Only the $schema URI changes; the keywords used here are valid in both.
Required
Every key present makes the schema strict: the example document defines the minimum. That is what you want for a config where a missing key breaks the service. Nothing required produces a shape-only schema — types are checked, presence is not.
Extra keys
Omitting additionalProperties allows unknown keys, which keeps the schema forward-compatible. Setting it to false turns a typo like prot = 8080 into a validation error — strict and usually worth it for a config you control. true states the permissive intent explicitly.
Examples / Title / \$id
Examples copies the input's values into the schema, which makes editor tooltips much more useful — but remember the values come from your file, so do not include a config full of secrets. Title and $id are metadata: a human name and the canonical URI where the schema will live.
Example
Input:
[[ports]]
name = "http"
port = 8080
[[ports]]
name = "metrics"
port = 9100
expose = false
Output (trimmed):
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"ports": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"port": { "type": "integer" },
"expose": { "type": "boolean" }
},
"required": ["name", "port"]
}
}
},
"required": ["ports"]
}
expose is in properties but not in required, because only the second entry has it. That intersection rule is what stops a generated schema from rejecting perfectly good configs.
Limits and notes
- It generalises from one example. Anything absent from your input is absent from the schema. Feed it the most complete config you have.
- No value constraints. Types and presence only — no
minimum,pattern, orenum. Those need a human decision; add them to the generated file. - Dates become strings. JSON Schema has no date type.
format: date-timeis advisory: many validators do not enforce formats unless you turn that on. - TOML integers can exceed JSON safety. A 64-bit integer beyond 2^53 is still described as
integer, but a JavaScript-based validator may lose precision reading the value itself.
FAQ
Can a JSON Schema validate a TOML file?
Yes, once the TOML is parsed. TOML and JSON share a data model for objects, arrays, strings, numbers, and booleans, so nearly every tool that validates pyproject.toml works exactly this way. Only date-times need the string mapping.
Why is a key missing from required?
Because it was missing from at least one element of an array of tables. Required for array items is the intersection across all elements, which keeps the schema from rejecting valid documents.
Should I set additionalProperties to false?
For a config you own, usually yes — it turns a misspelled key into an error instead of a silently ignored line. Leave it open if third parties extend the file, or if plugins add their own sections.
Which draft do editors want?
Most modern editors accept both. If your setup is fussy, draft-07 has the widest support in older tooling; 2020-12 is the current specification and the better default for new schemas.
Does including examples leak data?
It copies real values from your input into the schema, so yes — if the config holds secrets, either leave examples off or run the file through Redact first.