API DTOs (Pydantic)¶
Generate Pydantic Data Transfer Objects for building REST APIs from your TypeDB schema.
Overview¶
The DTO generator creates Pydantic models that mirror your TypeDB schema, providing:
- Type-safe API contracts - Request/response validation via Pydantic
- Schema-driven generation - DTOs stay in sync with your database schema
- Discriminated unions - Handle polymorphic entities with
typefield discrimination - Customizable output - Configure base classes, validators, field names, and more
Quick Start¶
# Programmatic
from type_bridge.generator import generate_models
generate_models("schema.tql", "./myapp/models/", generate_dto=True)
This generates api_dto.py alongside your other model files.
Generated Structure¶
For a schema with entities and relations, the generator creates:
# Base classes
class BaseDTO(BaseModel): ...
class BaseDTOOut(BaseDTO): ... # Has 'iid' field (TypeDB internal ID)
class BaseDTOCreate(BaseDTO): ...
class BaseDTOPatch(BaseDTO): ... # extra="forbid"
class BaseRelationOut(BaseDTO): ... # Has 'iid' and 'type'
class BaseRelationCreate(BaseDTO): ...
# Entity DTOs (for each non-abstract entity)
class PersonOut(BaseDTOOut): ...
class PersonCreate(BaseDTOCreate): ...
class PersonPatch(BaseDTOPatch): ...
# Relation DTOs (for each non-abstract relation)
class FriendshipOut(BaseRelationOut):
friend_iid: Optional[str] # Role player's TypeDB IID (for reading)
...
class FriendshipCreate(BaseRelationCreate):
friend_id: str # Role player identifier (for resolving)
...
# Union types (discriminated by 'type' field)
EntityOut = Annotated[Union[PersonOut, ...], Field(discriminator="type")]
EntityCreate = Annotated[Union[PersonCreate, ...], Field(discriminator="type")]
EntityPatch = Annotated[Union[PersonPatch, ...], Field(discriminator="type")]
RelationOut = Annotated[Union[FriendshipOut, ...], Field(discriminator="type")]
RelationCreate = Annotated[Union[FriendshipCreate, ...], Field(discriminator="type")]
Usage Example¶
from myapp.models.api_dto import PersonOut, PersonCreate, EntityOut
# FastAPI endpoint
@app.post("/persons", response_model=PersonOut)
def create_person(data: PersonCreate) -> PersonOut:
person = Person(name=Name(data.name), age=Age(data.age))
person_manager.insert(person)
return PersonOut(
iid=person.iid,
name=data.name,
age=data.age,
type="person"
)
# Polymorphic endpoint using discriminated union
@app.get("/entities/{id}", response_model=EntityOut)
def get_entity(id: str) -> EntityOut:
# Returns PersonOut, CompanyOut, etc. based on actual type
...
DTOConfig - Customization¶
For advanced use cases, configure generation with DTOConfig:
from type_bridge.generator import (
DTOConfig,
BaseClassConfig,
ValidatorConfig,
FieldSyncConfig,
generate_models,
)
config = DTOConfig(
# ... options ...
)
generate_models("schema.tql", "./models/", generate_dto=True, dto_config=config)
CLI with Config¶
Where myapp/config.py contains:
Configuration Options¶
DTOConfig Reference¶
| Option | Type | Default | Description |
|---|---|---|---|
base_classes |
list[BaseClassConfig] |
[] |
Custom base classes for entity hierarchies |
validators |
list[ValidatorConfig] |
[] |
Custom validator types with regex patterns |
preamble |
str \| None |
None |
Python code injected after imports |
entity_union_name |
str |
"Entity" |
Name prefix for entity union types |
relation_union_name |
str |
"Relation" |
Name prefix for relation union types |
exclude_entities |
list[str] |
[] |
Entity names to exclude from generation |
iid_field_name |
str |
"iid" |
Field name for TypeDB IID |
skip_relation_output |
bool |
False |
Skip generating relation Out classes |
relation_create_base_class |
str \| None |
None |
Custom base class for relation Create DTOs |
relation_preamble |
str \| None |
None |
Python code injected in relation section |
composite_entities |
list[CompositeEntityConfig] |
[] |
Composite (flat/merged) DTOs configuration |
strict_out_models |
bool |
False |
Required fields are non-Optional in Out DTOs |
entity_field_overrides |
list[EntityFieldOverride] |
[] |
Per-entity, per-variant field overrides |
BaseClassConfig Reference¶
| Option | Type | Default | Description |
|---|---|---|---|
source_entity |
str |
Required | Schema entity that triggers this base class |
base_name |
str |
Required | Base class name prefix (e.g., "BaseArtifact") |
inherited_attrs |
list[str] |
[] |
Attributes defined in base (skipped in children) |
extra_fields |
dict[str, str] |
{} |
Additional fields as {name: type_annotation} |
field_syncs |
list[FieldSyncConfig] |
[] |
Field sync validators for this base |
create_field_overrides |
dict[str, FieldOverride] |
{} |
Per-field overrides for Create variant |
ValidatorConfig Reference¶
| Option | Type | Default | Description |
|---|---|---|---|
name |
str |
Required | Validator type name (e.g., "DisplayId") |
pattern |
str \| None |
None |
Regex pattern for validation |
FieldSyncConfig Reference¶
| Option | Type | Description |
|---|---|---|
field_a |
str |
First field name |
field_b |
str |
Second field name |
When one field is set but not the other, the value is copied automatically.
CompositeEntityConfig Reference¶
Composite entities create a "flat" DTO that merges multiple entity types into one class. Useful for polymorphic APIs where one endpoint handles all node types.
| Option | Type | Default | Description |
|---|---|---|---|
name |
str |
Required | Name prefix (e.g., "GraphNode" → GraphNodeOut) |
base_entity |
str \| None |
None |
Entity name - include all inheriting entities |
include_entities |
list[str] |
[] |
Explicit list of entities to include |
exclude_entities |
list[str] |
[] |
Entities to exclude from the composite |
common_fields |
list[CompositeFieldConfig] |
[] |
Explicitly configured common fields |
field_syncs |
list[FieldSyncConfig] |
[] |
Field sync validators for the composite |
extra_fields |
dict[str, str] |
{} |
Additional fields as {name: type_annotation} |
extra_fields_out |
dict[str, str] |
{} |
Per-variant overrides for extra_fields on Out DTOs |
skip_variants |
set[str] |
{} |
Variant names to skip generating ("out", "create", "patch") |
id_field_name |
str |
"id" |
Name of the ID field |
type_field_name |
str |
"type" |
Name of the type discriminator field |
type_enum_from_registry |
bool |
True |
Generate Literal type enum from included entities |
Use base_entity OR include_entities, not both. When using base_entity, all non-abstract entities inheriting from it are included.
CompositeFieldConfig Reference¶
| Option | Type | Default | Description |
|---|---|---|---|
name |
str |
Required | Python field name (snake_case) |
type_annotation |
str |
Required | Python type annotation (e.g., "str", "int") |
default |
str \| None |
None |
Default value (e.g., "None", "''") |
description |
str \| None |
None |
Optional field description |
If default is None, the field is required in Create DTOs.
FieldOverride Reference¶
Override the requiredness or default of a single field on a specific DTO variant (used in BaseClassConfig.create_field_overrides).
| Option | Type | Default | Description |
|---|---|---|---|
required |
bool \| None |
None |
Override requiredness (None keeps schema default) |
default |
str \| None |
None |
Override default value as Python literal string |
EntityFieldOverride Reference¶
Per-entity, per-variant override targeting a specific (entity, field, variant) combination (used in DTOConfig.entity_field_overrides).
| Option | Type | Default | Description |
|---|---|---|---|
entity |
str |
Required | TypeDB entity name (e.g., "task") |
field |
str |
Required | TypeDB attribute name (e.g., "display_id") |
variant |
str |
Required | DTO variant: "create", "out", or "patch" |
required |
bool \| None |
None |
Override requiredness (None keeps schema default) |
default |
str \| None |
None |
Override default value as Python literal string |
Common Patterns¶
Composite DTOs for Polymorphic APIs¶
When your API needs to handle multiple entity types in one endpoint, use composite DTOs:
from type_bridge.generator import (
DTOConfig,
CompositeEntityConfig,
CompositeFieldConfig,
FieldSyncConfig,
)
config = DTOConfig(
composite_entities=[
CompositeEntityConfig(
name="GraphNode",
base_entity="artifact", # Include all entities inheriting from artifact
exclude_entities=["internal_utility"], # Exclude specific types
common_fields=[
CompositeFieldConfig("name", "str"),
CompositeFieldConfig("description", "str", default="''"),
CompositeFieldConfig("status", "str", default="'proposed'"),
],
field_syncs=[FieldSyncConfig("description", "content")],
extra_fields={"version": "int | None = None"},
),
]
)
This generates:
# Type enum (from included entities)
GraphNodeType = Literal["task", "epic", "story", ...]
class GraphNodeOut(BaseDTOOut):
type: GraphNodeType
name: Optional[str] = None
description: Optional[str] = None
status: Optional[str] = None
version: int | None = None
# ... merged attributes from all included entities
class GraphNodeCreate(BaseDTOCreate):
type: GraphNodeType
name: str # Required (no default)
description: Optional[str] = ''
status: Optional[str] = 'proposed'
version: int | None = None
class GraphNodePatch(BaseDTOPatch):
name: Optional[str] = None
description: Optional[str] = None
status: Optional[str] = None
version: Optional[int] = None
Composite DTOs are generated alongside per-entity DTOs, giving you both:
- Per-entity DTOs (
TaskOut,EpicOut) for type-specific endpoints - Composite DTOs (
GraphNodeOut) for polymorphic endpoints
Skipping Composite Variants¶
When you use discriminated unions (per-entity DTOs) instead of flat composite models, you can skip generating the composite Out/Create/Patch classes while keeping the type enum:
CompositeEntityConfig(
name="GraphNode",
include_entities=["task", "epic", "story"],
# Only generate GraphNodeType Literal enum;
# skip GraphNodeOut, GraphNodeCreate, GraphNodePatch
skip_variants={"out", "create", "patch"},
)
This generates just:
Without the composite GraphNodeOut, GraphNodeCreate, and GraphNodePatch classes. You can then build your own discriminated unions from the per-entity DTOs:
Per-Variant Extra Fields on Composites¶
When a composite's extra_fields need different type annotations per variant (e.g., id is required on Out but optional on Create), use extra_fields_out:
CompositeEntityConfig(
name="GraphNode",
include_entities=["task", "epic"],
extra_fields={
"id": "Optional[str] = None", # Default: optional (used for Create/Patch)
"version": "int | None = None",
},
extra_fields_out={
"id": "str", # Override: required on Out
},
)
This generates:
class GraphNodeOut(BaseDTOOut):
id: str # Required (from extra_fields_out)
version: int | None = None # From extra_fields
class GraphNodeCreate(BaseDTOCreate):
id: Optional[str] = None # Optional (from extra_fields)
version: int | None = None
Field Requiredness Overrides¶
@key fields are required on Create DTOs by default, but servers often auto-generate keys. Use overrides to make them optional on specific variants.
Base class level (applies to all entities using this base):
from type_bridge.generator import DTOConfig, BaseClassConfig, FieldOverride
config = DTOConfig(
base_classes=[
BaseClassConfig(
source_entity="artifact",
base_name="BaseArtifact",
inherited_attrs=["display_id", "name"],
create_field_overrides={
"display_id": FieldOverride(required=False), # Server auto-generates
},
),
],
)
Per-entity level (targets a specific entity + field + variant):
from type_bridge.generator import DTOConfig, EntityFieldOverride
config = DTOConfig(
entity_field_overrides=[
EntityFieldOverride(
entity="person",
field="email",
variant="create",
required=False,
default="None",
),
],
)
Overrides are scoped: a Create override does not affect Out or Patch variants, and a per-entity override does not leak to other entities.
Relation Role Field Naming¶
For relation DTOs, role player fields use different suffixes depending on the DTO type:
- Out models:
{role}_iid- Returns the TypeDB internal ID (IID) of the role player - Create models:
{role}_id- Accepts any identifier you can resolve to a role player
This distinction is intentional:
- Reading: When returning relation data, the IID uniquely identifies the connected entity
- Creating: When creating relations, you provide an ID (which could be a display ID, UUID, or IID) that your application resolves to the actual entity
# Out: returns the actual TypeDB IIDs
class EmploymentOut(BaseRelationOut):
employee_iid: Optional[str] # e.g., "0x826e80018000000000000001"
company_iid: Optional[str]
# Create: accepts identifiers your app can resolve
class EmploymentCreate(BaseRelationCreate):
employee_id: str # e.g., "EMP-123" or a UUID
company_id: str
Excluding Internal Entities¶
Exclude utility entities that shouldn't appear in your API:
Strict Out Models¶
By default, all fields in Out DTOs are Optional for safety. Enable strict_out_models to make required fields (those with @key or min>=1 cardinality) non-optional:
This generates:
# Without strict_out_models (default):
class PersonOut(BaseDTOOut):
name: Optional[str] = None # Even @key is Optional
# With strict_out_models=True:
class PersonOut(BaseDTOOut):
name: str # @key field is required
nickname: Optional[str] = None # Optional fields stay Optional
Use this when you want your API contracts to reflect the actual database guarantees.
Renaming IID Field¶
Use conventional id instead of TypeDB's iid:
Custom Entity Hierarchy Base Classes¶
When your schema has abstract entities (like artifact) with common attributes:
config = DTOConfig(
base_classes=[
BaseClassConfig(
source_entity="artifact",
base_name="BaseArtifact",
inherited_attrs=["display_id", "name", "description", "status", "created_at"],
extra_fields={"version": "int | None = None"},
field_syncs=[FieldSyncConfig("description", "content")],
),
],
)
This generates:
BaseArtifactOut,BaseArtifactCreate,BaseArtifactPatchwith common fields- Child entities (e.g.,
TaskOut) inherit fromBaseArtifactOutinstead ofBaseDTOOut - Child DTOs only include their own attributes, not inherited ones
Custom Validators¶
Add validated types for specific field formats:
config = DTOConfig(
validators=[
ValidatorConfig(name="DisplayId", pattern=r"^[A-Z]{1,5}-\d+$"),
ValidatorConfig(name="Email", pattern=r"^[^@]+@[^@]+\.[^@]+$"),
],
)
Generates:
_DISPLAYID_PATTERN = re.compile(r"^[A-Z]{1,5}-\d+$")
def _validate_displayid(value: str) -> str:
if not _DISPLAYID_PATTERN.match(value):
raise ValueError(f"'{value}' is not a valid DisplayId format.")
return value
DisplayId = Annotated[str, AfterValidator(_validate_displayid)]
Complex Validators via Preamble¶
For validators with complex logic (not just regex):
config = DTOConfig(
preamble='''
_DISPLAY_ID_PATTERN = re.compile(r"^[A-Z]{1,5}-\\d+$")
_UUID_PATTERN = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.I)
_IID_PATTERN = re.compile(r"^0x[0-9a-f]{20,24}$", re.I)
def _validate_node_id(value: str) -> str:
"""Accept display IDs, UUIDs, or TypeDB IIDs."""
if _DISPLAY_ID_PATTERN.match(value) or _UUID_PATTERN.match(value) or _IID_PATTERN.match(value):
return value
raise ValueError(f"'{value}' is not a valid node ID format.")
NodeId = Annotated[str, AfterValidator(_validate_node_id)]
''',
)
Custom Union Names¶
Rename union types for your API conventions.
Note: The generator validates that entity/relation names don't clash with union type names. If you have an entity named entity (which generates class Entity), the default entity_union_name="Entity" will raise an error:
ValueError: Entity 'entity' generates class name 'Entity' which clashes
with entity_union_name='Entity'. Use a different entity_union_name in
DTOConfig to avoid this conflict.
Simply choose a different union name:
config = DTOConfig(
entity_union_name="GraphNode", # GraphNodeOut, GraphNodeCreate, GraphNodePatch
relation_union_name="GraphRelation", # GraphRelationOut, GraphRelationCreate
)
Custom Relation Structure¶
For APIs that use generic source_id/target_id instead of role-specific fields:
config = DTOConfig(
skip_relation_output=True, # Don't generate FriendshipOut, etc.
relation_create_base_class="BaseRelationCreate", # Defined in preamble
relation_preamble='''
class GraphEdgeOut(BaseDTO):
"""Custom edge output for graph API."""
iid: str = Field(description="Relation IID")
source: str = Field(description="Source node IID")
target: str = Field(description="Target node IID")
source_display_id: str
target_display_id: str
type: str
role_from: str
role_to: str
class BaseRelationCreate(BaseDTO):
"""Base for relation creates with generic source/target."""
source_id: NodeId
target_id: NodeId
context_id: NodeId | None = None
''',
)
With this config:
- Relation Out classes are skipped (you use
GraphEdgeOut) - Relation Create classes inherit from your
BaseRelationCreate - Role-specific fields (
friend_id, etc.) are not generated - Only owned attributes are schema-driven
Full Example¶
A complete configuration for a graph-based API:
from type_bridge.generator import (
DTOConfig,
BaseClassConfig,
FieldSyncConfig,
ValidatorConfig,
generate_models,
)
config = DTOConfig(
# Exclude internal entities
exclude_entities=["display_id_counter", "schema_status"],
# Use conventional 'id' field
iid_field_name="id",
# Custom union names
entity_union_name="GraphNode",
relation_union_name="GraphRelation",
# Custom validators
validators=[
ValidatorConfig(name="DisplayId", pattern=r"^[A-Z]{1,5}-\d+$"),
],
# Complex validators
preamble='''
_UUID_PATTERN = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.I)
_IID_PATTERN = re.compile(r"^0x[0-9a-f]{20,24}$", re.I)
def _validate_node_id(value: str) -> str:
if _DISPLAYID_PATTERN.match(value) or _UUID_PATTERN.match(value) or _IID_PATTERN.match(value):
return value
raise ValueError(f"'{value}' is not a valid node ID.")
NodeId = Annotated[str, AfterValidator(_validate_node_id)]
''',
# Custom base class for artifacts
base_classes=[
BaseClassConfig(
source_entity="artifact",
base_name="BaseArtifact",
inherited_attrs=["display_id", "name", "description", "status", "priority", "created_at", "updated_at"],
extra_fields={"version": "int | None = None", "content": "str | None = None"},
field_syncs=[FieldSyncConfig("description", "content")],
),
],
# Custom relation structure
skip_relation_output=True,
relation_create_base_class="BaseRelationCreate",
relation_preamble='''
class GraphEdgeOut(BaseDTO):
"""API response for graph edges."""
iid: str = Field(description="Relation IID")
source: str = Field(description="Source node IID")
target: str = Field(description="Target node IID")
source_display_id: str
target_display_id: str
type: str
role_from: str
role_to: str
class BaseRelationCreate(BaseDTO):
"""Base for creating graph edges."""
source_id: NodeId
target_id: NodeId
context_id: NodeId | None = None
''',
)
generate_models("schema.tql", "./models/", generate_dto=True, dto_config=config)
See Also¶
- Code Generator - Full generator documentation
- Entities - Entity inheritance and ownership
- Relations - Relations, roles, and role players