Specification scope: Core only.
Tool: Structurize is the JSON Structure-focused command-line interface and conversion toolkit from the Avrotize project.
Sample JSON shows the values that happened to occur. It does not give those values stable names, declare their intended types, or turn repeated shapes into a reusable contract.
Structurize closes that first gap. Its json2s command analyzes JSON and
JSONL samples and produces a JSON Structure schema for review, validation, code
generation, and documentation. The result is an informed proposal, not proof of
the producer’s intent. Review it before treating it as a contract.
What Structurize is
Structurize is a schema conversion toolkit that transforms between various schema formats: JSON Schema, JSON Structure, Avro Schema, Protocol Buffers, XSD, and more. It also generates code in multiple languages (C#, Python, TypeScript, Java, Go, Rust, C++) and exports schemas to SQL databases and data formats like Parquet and Iceberg.
The tool ships under two package names — structurize and avrotize — both
sharing the same codebase. Choose whichever aligns with your primary use case.
JSON Structure users will likely prefer structurize.
Install it with:
pip install structurize
Schema inference with json2s
The json2s command reads one or more JSON files and infers a JSON Structure
schema from them. It handles single JSON objects, JSON arrays, and JSONL
(newline-delimited JSON) files.
Basic usage
structurize json2s data.json --out schema.jstruct.json --type-name MyType
Parameters:
<json_files...>— One or more JSON files to analyze--out— Output path for the JSON Structure schema (stdout if omitted)--type-name— Name for the root type (default: “Document”)--base-id— Base URI for$idgeneration (default: “https://example.com/”)--sample-size— Maximum records to sample (0 = all, default: 0)--infer-choices— Detect discriminated unions (more on this below)
Multiple files and JSONL
The command accepts multiple input files, merging their structures into a unified schema. This is useful when your data is split across files or when you want to analyze several examples together.
For JSONL files (one JSON object per line), the inferrer reads each line as a separate document and consolidates their structures.
# Multiple JSON files
structurize json2s orders.json users.json events.json --out unified.jstruct.json
# A JSONL file with many records
structurize json2s events.jsonl --out events.jstruct.json --type-name DomainEvent
Detect discriminated unions with --infer-choices
Many event-driven systems, APIs, and message formats use discriminated
unions: a single field (often called
type, kind, or event_type) determines which variant of a structure you’re
dealing with.
Consider this JSONL file with three event types:
{"event_type": "user_created", "user_id": "u123", "email": "alice@example.com", "created_at": "2026-02-04T10:00:00Z"}
{"event_type": "user_created", "user_id": "u456", "email": "bob@example.com", "created_at": "2026-02-04T11:00:00Z"}
{"event_type": "order_placed", "order_id": "ord-001", "user_id": "u123", "total": 99.50, "items": [{"sku": "A1", "qty": 2}]}
{"event_type": "order_placed", "order_id": "ord-002", "user_id": "u456", "total": 150.00, "items": [{"sku": "B2", "qty": 1}]}
{"event_type": "payment_received", "payment_id": "pay-001", "order_id": "ord-001", "amount": 99.50, "method": "card"}
{"event_type": "payment_received", "payment_id": "pay-002", "order_id": "ord-002", "amount": 150.00, "method": "paypal"}
Without --infer-choices: a flat object
Running the basic inference:
structurize json2s events.jsonl --out events.jstruct.json --type-name DomainEvent
Produces a single object type with all fields merged:
Generated output: events.jstruct.json
{
"$schema": "https://json-structure.org/meta/core/v0/#",
"$id": "https://example.com/DomainEvent",
"type": "object",
"name": "DomainEvent",
"properties": {
"amount": {
"type": "double"
},
"created_at": {
"type": "datetime"
},
"email": {
"type": "string"
},
"event_type": {
"type": "string"
},
"items": {
"type": "array",
"items": {
"type": "object",
"name": "DomainEvent_items",
"properties": {
"qty": {
"type": "integer"
},
"sku": {
"type": "string"
}
},
"required": [
"qty",
"sku"
]
}
},
"method": {
"type": "string"
},
"order_id": {
"type": "string"
},
"payment_id": {
"type": "string"
},
"total": {
"type": "double"
},
"user_id": {
"type": "string"
}
},
"required": [
"event_type"
]
}
This works, but it loses the structure: email only makes sense for
user_created events, items only for order_placed, and so on. All fields
become optional except event_type, which is the only one present in every
record.
With --infer-choices: an inline union
Add the --infer-choices flag:
structurize json2s events.jsonl --infer-choices --out events.jstruct.json --type-name DomainEvent
Now the inferrer detects that event_type is a discriminator whose values
correlate with distinct field signatures. It produces a JSON Structure
choice type — an inline union:
Generated output: events.jstruct.json
{
"$schema": "https://json-structure.org/meta/core/v0/#",
"$id": "https://example.com/DomainEvent",
"type": "choice",
"name": "DomainEvent",
"$extends": "#/definitions/DomainEventBase",
"selector": "event_type",
"choices": {
"order_placed": {
"type": {
"$ref": "#/definitions/order_placed"
}
},
"payment_received": {
"type": {
"$ref": "#/definitions/payment_received"
}
},
"user_created": {
"type": {
"$ref": "#/definitions/user_created"
}
}
},
"definitions": {
"DomainEventBase": {
"abstract": true,
"type": "object",
"name": "DomainEventBase",
"properties": {
"event_type": {
"type": "string"
}
}
},
"order_placed": {
"type": "object",
"name": "order_placed",
"$extends": "#/definitions/DomainEventBase",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"name": "DomainEvent_items",
"properties": {
"qty": {
"type": "integer"
},
"sku": {
"type": "string"
}
},
"required": [
"qty",
"sku"
]
}
},
"order_id": {
"type": "string"
},
"total": {
"type": "double"
},
"user_id": {
"type": "string"
}
},
"required": [
"items",
"order_id",
"total",
"user_id"
]
},
"payment_received": {
"type": "object",
"name": "payment_received",
"$extends": "#/definitions/DomainEventBase",
"properties": {
"amount": {
"type": "double"
},
"method": {
"type": "string"
},
"order_id": {
"type": "string"
},
"payment_id": {
"type": "string"
}
},
"required": [
"amount",
"method",
"order_id",
"payment_id"
]
},
"user_created": {
"type": "object",
"name": "user_created",
"$extends": "#/definitions/DomainEventBase",
"properties": {
"created_at": {
"type": "datetime"
},
"email": {
"type": "string"
},
"user_id": {
"type": "string"
}
},
"required": [
"created_at",
"email",
"user_id"
]
}
}
}
This is a proper inline union:
selectorpoints to the discriminator field (event_type)choicesmaps each discriminator value to a variant type$extendsreferences an abstract base type with common fields- Each variant extends the base and adds its specific fields
The choice keys (order_placed, payment_received, user_created) match the
actual values in the data, so instances validate correctly.
Validate the result
Using the json-structure Python SDK, we can verify that both the schema and the original instances are valid:
import json
from json_structure import SchemaValidator, InstanceValidator
with open('events_schema.jstruct.json') as f:
schema = json.load(f)
# Validate the schema itself
sv = SchemaValidator(extended=True)
errors = sv.validate(schema)
print('Schema valid:', len(errors) == 0)
# Validate each instance
iv = InstanceValidator(schema, extended=True)
with open('events.jsonl') as f:
for line in f:
if line.strip():
instance = json.loads(line)
errors = iv.validate(instance)
print(f"{instance['event_type']}: {'valid' if not errors else errors}")
Output:
Schema valid: True
user_created: valid
user_created: valid
order_placed: valid
order_placed: valid
payment_received: valid
payment_received: valid
All six instances validate against the inferred schema.
How the algorithm works
The --infer-choices option uses a clustering algorithm:
-
Document Fingerprinting: Each JSON object is characterized by its field signature — the set of top-level keys it contains.
-
Jaccard Similarity Clustering: Documents with similar field signatures are grouped together. A two-pass refinement handles edge cases.
-
Discriminator Detection: The algorithm looks for fields whose values correlate strongly with cluster membership. A field like
event_typethat has distinct values for each cluster is a strong discriminator candidate. -
Sparse Data Filtering: If documents have high overlap (same basic structure with some optional fields), they’re treated as a single type with optional properties rather than distinct variants.
-
Nested Discriminators: The algorithm can detect discriminators inside nested objects (up to 2 levels deep), handling envelope patterns like CloudEvents with typed payloads.
The result is a schema that captures the polymorphic structure of your data rather than flattening everything into a single bag of optional fields.
Use cases
- Event Sourcing: Infer schemas from event logs with multiple event types
- API Documentation: Generate schemas from sample API responses
- Message Queues: Document Kafka/RabbitMQ message formats
- Data Lake Schemas: Create schemas for semi-structured data in Parquet or Iceberg
- Code Generation: Feed the schema into structurize’s code generators to produce typed classes
Get started
Install structurize:
pip install structurize
Point it at your data:
structurize json2s your-data.jsonl --infer-choices --out schema.jstruct.json --type-name YourType
Validate the result with the json-structure SDK, or use structurize to convert the schema to code, documentation, or other formats.