Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a323995
Add dynamic model generation support
koxudaxi Jan 2, 2026
b46fa8b
Add dynamic model generation support
koxudaxi Jan 2, 2026
27ac766
Move dynamic model generation to separate module with lazy import
koxudaxi Jan 2, 2026
2d7ab5e
Move imports to top level except circular import avoidance
koxudaxi Jan 2, 2026
7bc4a21
Add create_module_getattr helper for DRY lazy imports
koxudaxi Jan 2, 2026
46f7a71
Move dynamic.py imports to top level and use direct import in __init_…
koxudaxi Jan 2, 2026
b266a57
Add multi-module support and simplify thread lock
koxudaxi Jan 2, 2026
9d9c8bb
Refactor tests with assert helpers and DRY patterns
koxudaxi Jan 2, 2026
f2ddb50
Refactor to use stable_toposort and ast-based import parsing
koxudaxi Jan 2, 2026
091770b
Refactor tests to use model_validate assertions
koxudaxi Jan 2, 2026
751b28d
Add generate_and_validate helper with inline-snapshot
koxudaxi Jan 2, 2026
a1be68f
Use assert_dynamic_models helper with external_file
koxudaxi Jan 2, 2026
e90286a
Fix test helper to use external_file for JSON comparison
koxudaxi Jan 2, 2026
be0ed14
Remove inline comment from test helper
koxudaxi Jan 2, 2026
c507679
Remove inline comments and refactor with walrus operator
koxudaxi Jan 2, 2026
4e94952
Extract OpenAPI schema to external file
koxudaxi Jan 2, 2026
8044952
Add tests to achieve 100% coverage for dynamic.py
koxudaxi Jan 2, 2026
9792cbf
Fix thread safety issues in dynamic model generation
koxudaxi Jan 2, 2026
3b1bef0
Refactor cache key to preserve list order and remove dead code
koxudaxi Jan 2, 2026
b70edb0
Make dynamic module imports lazy
koxudaxi Jan 2, 2026
93b0696
Fix partial coverage in test cleanup
koxudaxi Jan 2, 2026
253190c
Add documentation for dynamic model generation
koxudaxi Jan 2, 2026
8cf8bdf
Add dynamic model generation to docs navigation
koxudaxi Jan 2, 2026
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
301 changes: 301 additions & 0 deletions docs/dynamic-model-generation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
# Dynamic Model Generation

Generate real Python model classes from JSON Schema or OpenAPI at runtime without writing files.

## Overview

While `generate()` produces source code as strings, `generate_dynamic_models()` creates actual Python classes that you can use immediately for validation and data processing. This is useful for:

- Runtime schema validation without code generation step
- Dynamic API clients that adapt to schema changes
- Testing and prototyping
- Plugin systems with dynamic schemas

## Quick Start

```python
from datamodel_code_generator import generate_dynamic_models

schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer"}
},
"required": ["name"]
}

models = generate_dynamic_models(schema)
User = models["Model"]

# Use the model for validation
user = User(name="Alice", age=30)
print(user.model_dump()) # {'name': 'Alice', 'age': 30}

# Validation errors are raised
try:
User(age="not a number") # Missing required 'name', wrong type for 'age'
except Exception as e:
print(e)
```

## API Reference

### `generate_dynamic_models()`

```python
def generate_dynamic_models(
input_: Mapping[str, Any],
*,
config: GenerateConfig | None = None,
cache_size: int = 128,
) -> dict[str, type]:
```

**Parameters:**

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `input_` | `Mapping[str, Any]` | required | JSON Schema or OpenAPI schema as dict |
| `config` | `GenerateConfig \| None` | `None` | Generation options (same as `generate()`) |
| `cache_size` | `int` | `128` | Maximum cached schemas. Set to `0` to disable |

**Returns:** `dict[str, type]` - Dictionary mapping class names to model classes.

### `clear_dynamic_models_cache()`

```python
def clear_dynamic_models_cache() -> int:
```

Clears the internal cache and returns the number of entries cleared.

## Examples

### JSON Schema with Nested Models

```python
from datamodel_code_generator import generate_dynamic_models

schema = {
"$defs": {
"Address": {
"type": "object",
"properties": {
"street": {"type": "string"},
"city": {"type": "string"}
},
"required": ["street", "city"]
},
"Person": {
"type": "object",
"properties": {
"name": {"type": "string"},
"address": {"$ref": "#/$defs/Address"}
},
"required": ["name"]
}
},
"$ref": "#/$defs/Person"
}

models = generate_dynamic_models(schema)

# Both models are available
Person = models["Person"]
Address = models["Address"]

person = Person(
name="Bob",
address={"street": "123 Main St", "city": "NYC"}
)
print(person.model_dump())
# {'name': 'Bob', 'address': {'street': '123 Main St', 'city': 'NYC'}}
```

### OpenAPI Schema

OpenAPI schemas are auto-detected:

```python
from datamodel_code_generator import generate_dynamic_models

openapi_schema = {
"openapi": "3.0.0",
"info": {"title": "User API", "version": "1.0.0"},
"paths": {},
"components": {
"schemas": {
"User": {
"type": "object",
"properties": {
"id": {"type": "integer"},
"email": {"type": "string", "format": "email"}
},
"required": ["id", "email"]
}
}
}
}

models = generate_dynamic_models(openapi_schema)
User = models["User"]

user = User(id=1, email="alice@example.com")
```

### With Custom Configuration

```python
from datamodel_code_generator import generate_dynamic_models, GenerateConfig, DataModelType

schema = {"type": "object", "properties": {"name": {"type": "string"}}}

config = GenerateConfig(
class_name="Customer",
output_model_type=DataModelType.PydanticV2BaseModel,
)

models = generate_dynamic_models(schema, config=config)
Customer = models["Customer"]
```

### Enum Models

```python
from datamodel_code_generator import generate_dynamic_models

schema = {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["pending", "approved", "rejected"]
}
}
}

models = generate_dynamic_models(schema)
Model = models["Model"]
Status = models["Status"]

# Enum validation
item = Model(status="approved")
print(item.status) # Status.approved
print(item.status.value) # 'approved'

# Invalid enum value raises error
try:
Model(status="invalid")
except Exception as e:
print(e)
```

### Circular References

```python
from datamodel_code_generator import generate_dynamic_models

schema = {
"$defs": {
"Node": {
"type": "object",
"properties": {
"value": {"type": "string"},
"children": {
"type": "array",
"items": {"$ref": "#/$defs/Node"}
}
}
}
},
"$ref": "#/$defs/Node"
}

models = generate_dynamic_models(schema)
Node = models["Node"]

tree = Node(
value="root",
children=[
Node(value="child1", children=[]),
Node(value="child2", children=[
Node(value="grandchild", children=[])
])
]
)
```

## Caching

Models are cached by schema content and configuration to avoid regeneration:

```python
from datamodel_code_generator import generate_dynamic_models, clear_dynamic_models_cache

schema = {"type": "object", "properties": {"x": {"type": "integer"}}}

# First call generates models
models1 = generate_dynamic_models(schema)

# Second call returns cached models (same object)
models2 = generate_dynamic_models(schema)
assert models1 is models2 # True

# Disable caching for specific call
models3 = generate_dynamic_models(schema, cache_size=0)
assert models1 is not models3 # True

# Clear all cached models
cleared = clear_dynamic_models_cache()
print(f"Cleared {cleared} cached schemas")
```

## Thread Safety

`generate_dynamic_models()` is thread-safe. Multiple threads can safely call it concurrently:

```python
import threading
from datamodel_code_generator import generate_dynamic_models

schema = {"type": "object", "properties": {"x": {"type": "integer"}}}
results = []

def worker():
models = generate_dynamic_models(schema)
results.append(models)

threads = [threading.Thread(target=worker) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()

# All threads get the same cached models
assert all(r is results[0] for r in results)
```

## Limitations

| Limitation | Details |
|------------|---------|
| Pydantic v2 only | Pydantic v1 is not supported |
| Not pickle-able | Use `model_dump()` to serialize instances |
| Dict input only | Schema must be a `dict`, not a file path or string |

## Comparison with `generate()`

| Feature | `generate()` | `generate_dynamic_models()` |
|---------|-------------|----------------------------|
| Output | Source code string | Actual Python classes |
| Use case | Code generation, file output | Runtime validation |
| Caching | No | Yes (configurable) |
| Thread-safe | Yes | Yes |
| Pydantic v1 | Yes | No |

## See Also

- [Using as Module](using_as_module.md) - `generate()` function reference
- [JSON Schema](jsonschema.md) - JSON Schema examples
- [OpenAPI](openapi.md) - OpenAPI examples
5 changes: 3 additions & 2 deletions docs/using_as_module.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,5 +251,6 @@ class Model(BaseModel):

## 📖 See Also

- 🖥️ [CLI Reference](cli-reference/index.md) - Complete CLI options (same parameters as module)
- 📋 [Generate from JSON Schema](jsonschema.md) - JSON Schema examples
- [Dynamic Model Generation](dynamic-model-generation.md) - Generate Python classes at runtime
- [CLI Reference](cli-reference/index.md) - Complete CLI options (same parameters as module)
- [Generate from JSON Schema](jsonschema.md) - JSON Schema examples
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ lint.per-file-ignores."scripts/*.py" = [
lint.per-file-ignores."tests/**/*.py" = [
"FBT", # don't care about booleans as positional arguments in tests
"INP001", # no implicit namespace
"N806", # variable names for model classes can be uppercase
"PLC0415", # local imports in tests are fine
"PLC2701", # private import is fine
"PLR0904", # too many public methods in test classes is fine
Expand Down
21 changes: 20 additions & 1 deletion src/datamodel_code_generator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ def chdir(path: Path | None) -> Iterator[None]:
os.chdir(prev_cwd)


def is_openapi(data: dict) -> bool:
def is_openapi(data: Mapping[str, Any]) -> bool:
"""Check if the data dict is an OpenAPI specification."""
return "openapi" in data

Expand Down Expand Up @@ -927,6 +927,23 @@ def infer_input_type(text: str) -> InputFileType:
"`--input-file-type` option."
)


_LAZY_IMPORTS = {
"clear_dynamic_models_cache": "datamodel_code_generator.dynamic",
"generate_dynamic_models": "datamodel_code_generator.dynamic",
}


def __getattr__(name: str) -> Any:
if name in _LAZY_IMPORTS:
import importlib # noqa: PLC0415

module = importlib.import_module(_LAZY_IMPORTS[name])
return getattr(module, name)
msg = f"module {__name__!r} has no attribute {name!r}"
raise AttributeError(msg)


__all__ = [
"DEFAULT_FORMATTERS",
"DEFAULT_SHARED_MODULE_NAME",
Expand Down Expand Up @@ -959,5 +976,7 @@ def infer_input_type(text: str) -> InputFileType:
"ReuseScope",
"SchemaParseError",
"TargetPydanticVersion",
"clear_dynamic_models_cache", # noqa: F822
"generate",
"generate_dynamic_models", # noqa: F822
]
Loading
Loading