Pydantic-Based Extraction Templates#
In docling-graph, a template is a plain Python file of Pydantic models that tells the extraction pipeline what entities and relationships to find in a document and how to write them into a knowledge graph. Every structural decision — which models become graph nodes, how nodes are identified for deduplication, and which fields become edges — is expressed through standard Pydantic primitives (ConfigDict, Field, json_schema_extra), not through a separate DSL or configuration layer.
Core Configuration Fields#
Three custom model_config keys control graph structure :
| Key | Type | Effect |
|---|---|---|
graph_id_fields | list[str] | Marks a model as an entity; the listed field names form its stable identity for cross-batch deduplication |
is_entity | bool (default True) | When set to False, marks a model as a component (value object); no graph node is created |
graph_max_instances | int | Optional upper bound on the number of instances retained after graph assembly |
At conversion time, GraphConverter reads these values through the get_model_config_value() helper . The NodeIDRegistry._generate_fingerprint() method then :
- Entities (
graph_id_fieldsset): fingerprint from the declared identity fields only, enabling stable deduplication across extraction batches. - Components (
is_entity=False): fingerprint from all non-empty scalar fields, giving content-based deduplication without a named identity.
Entities vs. Components#
Entities become graph nodes. They need graph_id_fields set to required, scalar, short, verbatim-copyable values — ideally one field, two at most . The identity key must be something the document actually names; never instruct the model to invent one, because invented IDs differ between batches and break deduplication .
class Organization(BaseModel):
model_config = ConfigDict(graph_id_fields=["name"])
name: str = Field(...)
Components (is_entity=False) are value objects embedded inside the nearest entity ancestor — no node is created for them . Use them for addresses, monetary amounts, tax brackets, and any value that only makes sense in context of its parent entity. Entities nested inside components still become full nodes; their edge is attached to the nearest enclosing entity, not the component wrapper .
class Address(BaseModel):
model_config = ConfigDict(is_entity=False)
street: str = Field(...)
city: str = Field(...)
The practical rule: components cost nothing in the extraction catalog; entities each add a discovery pass and a fill pass .
Edge Definitions#
Relationships become graph edges through the edge() helper, which must appear identically in every template :
def edge(label: str, **kwargs: Any) -> Any:
return Field(..., json_schema_extra={"edge_label": label}, **kwargs)
The edge_label key in json_schema_extra is the contract point the converter reads . Labels must be ALL_CAPS_WITH_UNDERSCORES verb phrases . List edges require default_factory=list .
Reference edges — fields that link to an entity by identity only, without extracting its full attributes — add reference=True, which sets json_schema_extra={"graph_reference": True} . Reference fields have no discovery path in Phase 1; the parent's fill call emits them as ID-only objects, and graph assembly resolves those onto the canonical node. This avoids the three major failure modes of full nested entities: per-parent membership collapse, parent drift, and catalog explosion .
File Organization#
Templates follow a strict declaration order so forward-reference errors are impossible :
- Imports
edge()helper- Helper/utility functions
- Components (
is_entity=False) - Reusable entities (e.g.,
Person,Organization) - Domain-specific models
- Root document model (last)
The root model is the extraction entry point and always has graph_id_fields set .
Key Design Rules (Quick Reference)#
- Identity fields: required, scalar, ≤2 fields, verbatim-copyable, never invented
- All non-identity fields: optional (
| Noneordefault_factory=list) — required non-identity fields cause all-or-nothing validation loss on smaller models - Docstrings: front-load the discriminating sentence in the first ~240 chars; Phase 1 only sees those 240 chars for entity classification
- Validators: normalize, never reject; use
mode="before"to coerce what LLMs actually emit - One canonical home per entity: if the same rich model appears at multiple paths, all but one should use
reference=True graph_max_instances: set to ~2× the documented maximum on classes prone to over-discovery
Primary References#
| Source | What it covers |
|---|---|
| template-basics.md | Required imports, edge() definition, file organization |
| relationships.md | Edge syntax, label conventions, reference edges, closed-catalog edges |
| best-practices.md | Identity design, token economics, graph assembly mechanics, failure modes |
| billing_document.py | Complete real-world template (invoice/billing domain) |
| entities-vs-components.md | Entity vs. component decision guide |
| GraphConverter | Conversion-time consumption of graph_id_fields, is_entity, edge_label |
| NodeIDRegistry | Fingerprint generation from graph_id_fields |