Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ indent = 4
verbose = false
schema_suffix = ".schema.json"
overwrite = true
single_file = false
single_file_name = "schemas.json"
```

### CLI Framework
Expand All @@ -131,6 +133,42 @@ The `SchemaWriter` class handles:
3. JSON schema generation using Pydantic's built-in `model_json_schema()`
4. File output with configurable formatting

#### Single-File Mode

The tool supports generating a single consolidated schema file that conforms to JSON Schema 2020-12 specification:

- Uses `$defs` to define all model schemas
- Uses `$ref` constructs for referencing definitions
- Includes `$schema` pointing to `https://json-schema.org/draft/2020-12/schema`
- Configurable via `--single-file` CLI option or `single_file` config setting
- Custom filename via `--single-file-name` or `single_file_name` config setting

Example output structure:
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "file:///path/to/schemas.json",
"title": "Consolidated Pydantic Models Schema",
"description": "JSON Schema definitions for all Pydantic models",
"$defs": {
"User": { ... },
"Product": { ... }
}
}
```

Usage:
```bash
# Generate single consolidated schema
schemali models.py --single-file

# With custom filename
schemali models.py --single-file --single-file-name all-schemas.json

# Multiple modules into single file
schemali user.py product.py order.py --single-file
```

## Testing Strategy

### Test Coverage Goals
Expand Down
55 changes: 54 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ A modern CLI tool for generating JSON schemas from Pydantic models.
- 🚀 Load one or more Python modules containing Pydantic models
- 🔍 Automatically discover all Pydantic models in each module
- 📋 Generate JSON schemas compliant with JSON Schema specification
- 💾 Write schemas to individual files
- 💾 Write schemas to individual files or a single consolidated file
- 📦 Single-file mode with JSON Schema 2020-12 `$defs` and `$ref` support
- 🎨 Beautiful terminal output with colors and tables
- ⚙️ Flexible configuration via TOML files, environment variables, or CLI arguments
- 🧪 Comprehensive test coverage with pytest
Expand Down Expand Up @@ -56,6 +57,9 @@ schemali user.py product.py order.py
# Specify output directory
schemali models.py -o schemas/

# Generate single consolidated schema (JSON Schema 2020-12)
schemali models.py --single-file

# Use verbose output
schemali models.py -v
```
Expand Down Expand Up @@ -108,6 +112,51 @@ schemali models.py -v
schemali models.py --verbose
```

### Single Consolidated Schema File

Generate a single JSON Schema 2020-12 compliant file with all models using `$defs`:

```bash
# Generate single consolidated schema
schemali models.py --single-file

# With custom filename
schemali models.py --single-file --single-file-name all-schemas.json

# Multiple modules into one consolidated file
schemali user.py product.py order.py --single-file
```

This creates a schema file like:

```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "file:///path/to/schemas.json",
"title": "Consolidated Pydantic Models Schema",
"description": "JSON Schema definitions for all Pydantic models",
"$defs": {
"User": { /* User schema */ },
"Product": { /* Product schema */ },
"Order": { /* Order schema */ }
}
}
```

You can reference models using `$ref`:
```json
{
"type": "object",
"properties": {
"user": { "$ref": "#/$defs/User" },
"items": {
"type": "array",
"items": { "$ref": "#/$defs/Product" }
}
}
}
```

### Using a Configuration File

Create a `schemali.toml` configuration file:
Expand All @@ -119,6 +168,8 @@ indent = 4
verbose = false
schema_suffix = ".schema.json"
overwrite = true
single_file = false
single_file_name = "schemas.json"
```

Then run:
Expand Down Expand Up @@ -165,6 +216,8 @@ Schemali uses a flexible configuration system powered by **pydantic-settings**.
| `verbose` | bool | false | Enable verbose output |
| `schema_suffix` | str | `.schema.json` | Suffix for generated schema files |
| `overwrite` | bool | true | Whether to overwrite existing files |
| `single_file` | bool | false | Generate single consolidated schema file |
| `single_file_name` | str | `schemas.json` | Name of single output file |

### Configuration File Locations

Expand Down
1 change: 1 addition & 0 deletions examples/simple_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

class Book(BaseModel):
"""A simple book model."""

title: str
author: str
year: int
Expand Down
8 changes: 6 additions & 2 deletions examples/user_models.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
"""Example Pydantic models for testing."""

from typing import Optional, List
from datetime import datetime
from typing import List, Optional

from pydantic import BaseModel, EmailStr, Field


class Address(BaseModel):
"""User address information."""

street: str
city: str
state: str
zip_code: str = Field(..., pattern=r'^\d{5}(-\d{4})?$')
zip_code: str = Field(..., pattern=r"^\d{5}(-\d{4})?$")
country: str = "USA"


class User(BaseModel):
"""User model with various field types."""

id: int = Field(..., description="Unique user identifier")
username: str = Field(..., min_length=3, max_length=50)
email: EmailStr
Expand All @@ -29,6 +32,7 @@ class User(BaseModel):

class Product(BaseModel):
"""Product model."""

id: int
name: str = Field(..., min_length=1, max_length=200)
description: Optional[str] = None
Expand Down
103 changes: 87 additions & 16 deletions schemali/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ def main(
"--verbose",
help="Enable verbose output",
),
single_file: bool = typer.Option(
False,
"--single-file",
help="Generate a single consolidated schema file using JSON Schema 2020-12 $defs",
),
single_file_name: Optional[str] = typer.Option(
None,
"--single-file-name",
help="Name of the single output file (default: schemas.json)",
),
config_file: Optional[Path] = typer.Option(
None,
"-c",
Expand Down Expand Up @@ -86,6 +96,12 @@ def main(
# Custom indentation and verbose output
schemali models.py --indent 4 -v

# Generate a single consolidated schema file (JSON Schema 2020-12)
schemali models.py --single-file

# Single file with custom name
schemali models.py --single-file --single-file-name all-schemas.json

# Use a configuration file
schemali models.py -c config.toml
"""
Expand All @@ -100,6 +116,10 @@ def main(
config.indent = indent
if verbose:
config.verbose = verbose
if single_file:
config.single_file = single_file
if single_file_name is not None:
config.single_file_name = single_file_name

# Validate modules are Python files
for module_path in modules:
Expand All @@ -119,40 +139,91 @@ def main(
# Track results
total_models = 0
all_results = {}
all_models = []

# Process each module
# Process each module to discover models
for module_path in modules:
if config.verbose:
console.print(f"\n[bold]Processing module:[/bold] {module_path}")

results = writer.process_module(
module_path,
# Load the module
module = writer.load_module_from_path(module_path)

# Discover Pydantic models
models = writer.discover_pydantic_models(module)
all_models.extend(models)

if config.verbose:
model_names = [m.__name__ for m in models]
console.print(f"Found {len(models)} Pydantic model(s): {model_names}")

total_models = len(all_models)

# Generate schemas based on mode
if config.single_file:
# Single consolidated schema file
output_path = config.output_dir or Path.cwd()
if config.output_dir:
output_path = Path(config.output_dir)
else:
output_path = Path.cwd()

schema_file_path = output_path / config.single_file_name

result_path = writer.write_consolidated_schema(
all_models,
output_path=schema_file_path,
indent=config.indent,
verbose=config.verbose,
)

total_models += len(results)
all_results.update(results)
if config.verbose:
console.print(
f"\n[bold green]✓ Generated consolidated schema:[/bold green] {result_path}"
)

all_results["__consolidated__"] = result_path
else:
# Individual schema files for each model
for model in all_models:
schema_path = writer.write_schema(model, indent=config.indent)
all_results[model.__name__] = schema_path

if config.verbose:
console.print(f" ✓ {model.__name__} -> {schema_path}")

# Display summary
if total_models == 0:
console.print("[yellow]No Pydantic models found in the provided modules[/yellow]")
raise typer.Exit(0)

if not config.verbose:
# Create a nice table for non-verbose output
table = Table(title=f"\n✓ Successfully generated {total_models} schema(s)")
table.add_column("Model", style="cyan", no_wrap=True)
table.add_column("Schema File", style="green")
if config.single_file:
# Single file output
console.print(
f"\n[bold green]✓ Successfully generated consolidated schema[/bold green]\n"
f" Models: {total_models}\n"
f" File: {all_results['__consolidated__']}"
)
else:
# Create a nice table for non-verbose output
table = Table(title=f"\n✓ Successfully generated {total_models} schema(s)")
table.add_column("Model", style="cyan", no_wrap=True)
table.add_column("Schema File", style="green")

for model_name, schema_path in all_results.items():
table.add_row(model_name, str(schema_path))
for model_name, schema_path in all_results.items():
table.add_row(model_name, str(schema_path))

console.print(table)
console.print(table)
else:
console.print(
f"\n[bold green]✓ Complete![/bold green] Generated {total_models} schemas"
)
if config.single_file:
console.print(
f"\n[bold green]✓ Complete![/bold green] "
f"Generated consolidated schema with {total_models} models"
)
else:
console.print(
f"\n[bold green]✓ Complete![/bold green] Generated {total_models} schemas"
)

except KeyboardInterrupt:
console.print("\n[yellow]Interrupted by user[/yellow]")
Expand Down
10 changes: 10 additions & 0 deletions schemali/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ class SchemaliConfig(BaseSettings):

overwrite: bool = Field(default=True, description="Whether to overwrite existing schema files")

# Single-file output configuration
single_file: bool = Field(
default=False, description="Generate a single consolidated schema file using $defs"
)

single_file_name: str = Field(
default="schemas.json",
description="Name of the single output file when single_file is enabled",
)

@classmethod
def load_config(cls, config_file: Optional[Path] = None) -> "SchemaliConfig":
"""Load configuration from a specific file or default locations.
Expand Down
Loading
Loading