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 $id generation (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:

  • selector points to the discriminator field (event_type)
  • choices maps each discriminator value to a variant type
  • $extends references 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:

  1. Document Fingerprinting: Each JSON object is characterized by its field signature — the set of top-level keys it contains.

  2. Jaccard Similarity Clustering: Documents with similar field signatures are grouped together. A two-pass refinement handles edge cases.

  3. Discriminator Detection: The algorithm looks for fields whose values correlate strongly with cluster membership. A field like event_type that has distinct values for each cluster is a strong discriminator candidate.

  4. 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.

  5. 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.