Avro Schema Generator
Paste sample JSON, get a valid Avro .avsc — nullable unions with defaults and honest int-versus-long, entirely on this device.
Inferred from what the samples show: a field no sample carries cannot be known, and an empty array has no item type. Read the schema before you register it.
Optional. Avro names joined by single dots, e.g. com.example — left empty, the schema carries no namespace.
Promoted only when every sample value at a path matches — RFC 3339 dates and date-times, RFC 9562 UUIDs. One value that does not match leaves the field a string.
The Avro schema appears here as you type.
Press ? for shortcuts
Do it offline:$ java -jar avro-tools-1.12.0.jar fromjson --schema-file schema.avsc records.json > records.avro
How to use it
- Paste one response, or drop a .json file on the left pane — the .avsc rebuilds as you type.
- Open a sample tab per payload, up to ten: a key missing from any tab comes back as ["null", T] with a null default.
- Set the record name and the namespace, then choose the non-integer width and whether dates and UUIDs are promoted.
- Copy the schema, download it as a .avsc file, or share a permalink that reopens the same tabs and the same options.
Paste sample JSON and get a valid Avro .avsc: nullable fields as ["null", T] carrying a null default, int against long decided by the raw token you pasted, nested objects as named record types defined once. Read what comes back as a starting point, not an authority — a field that appears in no sample cannot be known, an empty array has no item type, and bytes, fixed and decimal are unreachable from JSON text. Nothing is uploaded.
Examples
{"name": "Alyssa P. Hacker", "favorite_number": 256, "favorite_color": null}
{"name": "Ben", "favorite_number": 7, "favorite_color": "red"}{
"type": "record",
"name": "User",
"namespace": "example.avro",
"fields": [
{
"name": "name",
"type": "string"
},
{
"name": "favorite_number",
"type": "int"
},
{
"name": "favorite_color",
"type": [
"null",
"string"
],
"default": null
}
]
}
The Avro documentation's own User example, given the two payloads its author had. favorite_color is null once and "red" once, so it becomes a union — and "null" is written first because Avro resolves a field default against the first branch only.
{"post_id": 9223372036854775807, "views": 2147483647, "score": 1e2}{
"type": "record",
"name": "Root",
"fields": [
{
"name": "post_id",
"type": "long"
},
{
"name": "views",
"type": "int"
},
{
"name": "score",
"type": "double"
}
]
}
Three numbers, three Avro widths, read from the characters rather than from a parsed float: post_id needs all 64 bits, views still fits 32, and score is written in exponent form, which JSON leaves no way to call whole.
{"id": "0b5b8e6c-6a1d-4c2e-9b3a-5f6d7e8c9a0b", "created_at": "2026-08-11T09:15:00Z", "due": "2026-08-31"}{
"type": "record",
"name": "Task",
"namespace": "com.example.tasks",
"fields": [
{
"name": "id",
"type": {
"type": "string",
"logicalType": "uuid"
}
},
{
"name": "created_at",
"type": {
"type": "long",
"logicalType": "timestamp-millis"
}
},
{
"name": "due",
"type": {
"type": "int",
"logicalType": "date"
}
}
]
}
With both logical-type switches on, the promotion reads the values and never the key spelling: a field named created_at holding something other than a date-time would stay a plain string.
{"tags": [], "nickname": null}{
"type": "record",
"name": "Root",
"fields": [
{
"name": "tags",
"type": {
"type": "array",
"items": "null"
}
},
{
"name": "nickname",
"type": "null",
"default": null
}
]
}
The edge case worth seeing early. Neither field carries evidence, so both come back as placeholders you are meant to replace — an items type of "null" and a bare null field, each with the warning quoted below.
Every example on this page is executed against the same engine that powers the tool, on every build.
Gotchas worth knowing
Why "null" is always the first branch
A field default is matched against the first branch of its union, so "default": null is legal only when "null" leads. Turning the defaults switch off leaves the same branch order and drops the default line — useful when the writer must always supply a value.
Widening, never a union of numbers
int, long, float and double are promotable in that order, so a path seen as int in one payload and as a fraction in another is typed at the widest width instead of unioned — Avro rejects a union that could match two of them.
Names are rewritten; the JSON key is kept
A key like user-id becomes the field user_id, and the original spelling is preserved in the field's "doc" — never in "aliases", which must satisfy the same grammar. A record name colliding with a primitive gets a trailing underscore.
One shape is defined once and referenced after
When the same object shape turns up at two paths, the second field's type is the first record's fullname, not a second copy. Redefining a fullname is a hard error in every Avro parser, so this is the only output a reader will accept.
Common errors
Sample 2: Unexpected '}' at line 1, column 9 — probable cause: trailing comma after the last property.- Every tab has to hold strict JSON before the shapes are merged. The prefix names the failing tab and appears only when more than one is open; the caret frame marks the character, and Jump to error puts the cursor on it.
`com.example.` is not a legal Avro namespace, so no schema was generated — a namespace is one or more Avro names joined by single dots, with no leading, trailing or doubled dot.- Refusing beats emitting a document no parser will load. The same check guards the record name, and both run before a single sample is read, so the input pane is left untouched.
Input is larger than the 4 MB limit for this tool.- Past the combined-sample ceiling the run is refused whole, never silently trimmed. Ten representative records infer what ten thousand copies of one record do, and much faster.
integer 9223372036854775808 is beyond the range of Avro's `long`, so the field is typed `double` and the exact digits cannot survive — Avro has no arbitrary-precision integer, so carry a value this large as a string- A warning, not a refusal — the schema still generates. One digit lower is long's exact maximum and stays a long; there is nothing wider in the specification to promote to.
`nickname` was null in every sample, so its type is the bare `"null"` — add a sample with a real value, or replace the type by hand.- Chips sit under the output without blocking it. A field seen only as null has no second branch to offer, and inventing one would be a guess: paste a payload where the field is populated.
FAQ
How do I get an Avro map instead of a record?
By hand, on one line. An object with fixed keys and an object used as a dictionary look identical in JSON, so this page always writes a record; swap the generated record for {"type": "map", "values": …} where you know the keys are open-ended.
Can I use this schema to read Avro files I already have?
Only if it matches what the writer used. Reading is driven by the writer's schema plus resolution rules, and a schema inferred from decoded JSON knows nothing about the original field order, aliases or defaults. Treat it as a draft to reconcile.
Why did my timestamp field stay a plain string?
Either the logical-type switches are off — they are, by default — or one value in one tab did not match. Promotion is unanimous or it does not happen, because a single value outside the format makes the logical type wrong for the whole field.
Keyboard shortcuts
- ⌘/Ctrl + Enter
- Generate again straight away, which is how a large paste starts.
- ⌘/Ctrl + S
- Download the schema as a .avsc file rather than saving the page.
- ⌘/Ctrl + C
- With nothing selected, puts the whole .avsc on the clipboard.
- ⌘/Ctrl + Shift + C
- Take the whole schema even when a selection is live.
- t
- Flip the theme, unless a text field currently has focus.
- ? or ⌘/Ctrl + /
- Bring up the list of shortcuts.
- Esc
- Dismiss the topmost overlay.