Models
These are the core models used in structx for data extraction and results.
RowResult
Extraction outcome, provenance, and usage for one input row.
Attributes:
| Name |
Type |
Description |
position |
int
|
Zero-based input position, unique even when DataFrame index
labels are duplicated.
|
source_index |
Any
|
Original DataFrame index label.
|
input_data |
RowPayload
|
Exact text or PDF payload sent for this row.
|
items |
List[T]
|
Zero or more validated model instances returned for the row.
|
usage |
ExtractorUsage
|
Provider usage recorded by this row's extraction request. It does
not include operation-level schema planning.
|
error |
Optional[str]
|
Error text for a failed row, otherwise None.
|
Source code in structx/core/models.py
| @dataclass(frozen=True)
class RowResult(Generic[T]):
"""Extraction outcome, provenance, and usage for one input row.
Attributes:
position: Zero-based input position, unique even when DataFrame index
labels are duplicated.
source_index: Original DataFrame index label.
input_data: Exact text or PDF payload sent for this row.
items: Zero or more validated model instances returned for the row.
usage: Provider usage recorded by this row's extraction request. It does
not include operation-level schema planning.
error: Error text for a failed row, otherwise ``None``.
"""
position: int
source_index: Any
input_data: RowPayload
items: List[T]
usage: ExtractorUsage = field(default_factory=ExtractorUsage)
error: Optional[str] = None
@property
def status(self) -> str:
"""Return ``success``, ``empty``, or ``failed`` for this row."""
if self.error is not None:
return "failed"
return "success" if self.items else "empty"
|
status
property
Return success, empty, or failed for this row.
Container for extraction results.
Attributes:
| Name |
Type |
Description |
data |
Union[DataFrame, List[T]]
|
Extracted data (DataFrame or list of model instances)
|
rows |
List[RowResult[T]]
|
Row-level outcomes with provenance and usage
|
model |
Type[T]
|
Generated or provided model class
|
usage |
ExtractorUsage
|
Token usage information across all extraction steps
|
data is the convenient flattened output. rows is the canonical
mapping back to each input row and preserves empty results, failures, and
row-specific usage.
Source code in structx/core/models.py
| @dataclass
class ExtractionResult(Generic[T]):
"""
Container for extraction results.
Attributes:
data: Extracted data (DataFrame or list of model instances)
rows: Row-level outcomes with provenance and usage
model: Generated or provided model class
usage: Token usage information across all extraction steps
``data`` is the convenient flattened output. ``rows`` is the canonical
mapping back to each input row and preserves empty results, failures, and
row-specific usage.
"""
data: Union[pd.DataFrame, List[T]]
rows: List[RowResult[T]]
model: Type[T]
usage: ExtractorUsage = field(default_factory=ExtractorUsage)
@property
def failed(self) -> pd.DataFrame:
"""Failed row details as a compatibility-friendly DataFrame view."""
return pd.DataFrame.from_records(
[
{
"index": row.source_index,
"text": str(row.input_data),
"error": row.error,
}
for row in self.rows
if row.error is not None
],
columns=["index", "text", "error"],
)
@property
def attempted_count(self) -> int:
"""Number of input rows submitted for extraction."""
return len(self.rows)
@property
def success_count(self) -> int:
"""Number of input rows extracted successfully."""
return sum(row.error is None for row in self.rows)
@property
def empty_count(self) -> int:
"""Number of successful input rows that produced no model instances."""
return sum(row.status == "empty" for row in self.rows)
@property
def extracted_count(self) -> int:
"""Number of extracted model instances or result rows returned."""
return len(self.data)
@property
def failure_count(self) -> int:
"""Number of input rows that failed extraction."""
return len(self.failed)
@property
def success_rate(self) -> float:
"""Percentage of attempted rows that did not fail."""
total = self.attempted_count
return (self.success_count / total * 100) if total > 0 else 0
def __repr__(self) -> str:
"""String representation"""
return (
f"ExtractionResult(success={self.success_count}, "
f"failed={self.failure_count}, "
f"model={self.model.__name__})"
)
def __str__(self):
return self.__repr__()
|
Number of input rows submitted for extraction.
Number of successful input rows that produced no model instances.
Number of extracted model instances or result rows returned.
Failed row details as a compatibility-friendly DataFrame view.
Number of input rows that failed extraction.
Number of input rows extracted successfully.
Percentage of attempted rows that did not fail.
The distinction between flattened output and row provenance is covered in
Working with Results.
ModelField
Definition of a field in the extraction model
Source code in structx/core/models.py
| class ModelField(BaseModel):
"""Definition of a field in the extraction model"""
model_config = ConfigDict(frozen=True)
name: str = Field(description="Name of the field")
type: str = Field(
description=(
"Canonical Python type for the field, such as str, int, float, bool, "
"date, datetime, List[str], Dict[str, Any], or Optional[List[str]]"
)
)
description: str = Field(description="Description of what this field represents")
validation: Optional[Dict[str, Any]] = Field(
default_factory=dict, description="Additional validation rules"
)
nested_fields: Optional[List["ModelField"]] = Field(
default=None, description="Fields for nested models"
)
required: bool = Field(
default=False, description="Whether the generated field must be present"
)
nullable: bool = Field(
default=True, description="Whether the generated field may be null"
)
has_default: bool = Field(
default=False, description="Whether the generated field has an explicit default"
)
default: Any = Field(
default=None,
description="JSON-serializable default value when has_default is true",
)
@model_validator(mode="before")
@classmethod
def normalize_definition(cls, value: Any) -> Any:
if not isinstance(value, dict) or "type" not in value:
return value
normalized = value.copy()
field_type, validation = normalize_field_definition(
normalized["type"], normalized.get("validation")
)
normalized["type"] = field_type
normalized["validation"] = validation
if normalized.get("nullable") is False and field_type.startswith("Optional["):
normalized["type"] = field_type[len("Optional[") : -1]
return normalized
@model_validator(mode="after")
def validate_presence_semantics(self) -> "ModelField":
if self.required and self.has_default:
raise ValueError("A required field cannot also have a default")
if not self.required and not self.nullable and not self.has_default:
raise ValueError(
"A non-required field must be nullable unless it has an explicit default"
)
return self
|
Generated Field Types
ModelField normalizes model-generated type expressions before dynamic model
creation. Canonical expressions include:
- Scalars:
str, int, float, bool, date, datetime, time,
Decimal, UUID, and Any
- Collections:
List[T], Dict[str, T], Set[T], FrozenSet[T], and
Tuple[...]
- Nullable values:
Optional[T]
Common JSON, TypeScript, and legacy Pydantic forms such as string,
array<number>, string[], T | null, PositiveInt, and conlist(...) are
converted to canonical Python forms. Ambiguous unions and unknown types are
rejected instead of silently becoming strings.
Legacy validation names such as regex, min_items, and max_items are
normalized to their Pydantic v2 equivalents. Unsupported constraints and
invalid regular expressions are discarded before model creation.
Request for model generation
Source code in structx/core/models.py
| class ExtractionRequest(BaseModel):
"""Request for model generation"""
model_name: str = Field(description="Name for generated model")
model_description: str = Field(description="Description of model purpose")
fields: List[ModelField] = Field(description="Fields to extract")
|
ExtractionRequest is the portable representation of a StructX model. It can
be serialized as JSON, stored outside the Python process, and converted back to
a runtime Pydantic model:
from structx import (
ExtractionRequest,
model_from_extraction_request,
model_to_extraction_request,
)
definition = model_to_extraction_request(Invoice)
stored = definition.model_dump(mode="json")
restored_definition = ExtractionRequest.model_validate(stored)
RestoredInvoice = model_from_extraction_request(restored_definition)
StructX-generated and refined models retain their original definition, so
model_to_extraction_request() returns that definition without reverse
engineering it. Custom Pydantic models are converted from their declarative
fields, nested models, supported constraints, required state, nullability, and
serializable defaults.
Custom validators, serializers, computed fields, recursive models, and default
factories cannot be represented as portable data and are rejected rather than
silently discarded.
Type Capabilities
Use get_type_capabilities() to build schema editors without copying StructX's
private aliases or type parser:
from structx import get_type_capabilities
capabilities = get_type_capabilities()
payload = capabilities.model_dump(mode="json")
The catalog contains canonical scalar and container identifiers, user-facing
labels, supported constraints, valid item kinds, and presence modifiers.
Aliases such as string, integer, and array remain accepted as input but
are never returned by the catalog or persisted by ExtractionRequest.
Versioned canonical type catalog for external schema builders.
Source code in structx/schema.py
| class TypeCapabilities(BaseModel):
"""Versioned canonical type catalog for external schema builders."""
model_config = ConfigDict(frozen=True)
schema_version: str = "1"
scalars: List[ScalarTypeCapability]
containers: List[ContainerTypeCapability]
modifiers: TypeModifierCapabilities = Field(
default_factory=TypeModifierCapabilities
)
|
Schema Conversion Functions
Convert a declarative Pydantic model into a portable definition.
Source code in structx/schema.py
| def model_to_extraction_request(
model: Type[BaseModel],
*,
model_name: Optional[str] = None,
model_description: Optional[str] = None,
) -> ExtractionRequest:
"""Convert a declarative Pydantic model into a portable definition."""
if not isinstance(model, type) or not issubclass(model, BaseModel):
raise TypeError("model must be a Pydantic BaseModel class")
stored_definition = getattr(model, "__structx_definition__", None)
if isinstance(stored_definition, ExtractionRequest):
updates = {}
if model_name is not None:
updates["model_name"] = model_name
if model_description is not None:
updates["model_description"] = model_description
return stored_definition.model_copy(deep=True, update=updates)
description = model_description
if description is None:
description = (model.__dict__.get("__doc__") or "").strip()
return ExtractionRequest(
model_name=model_name or model.__name__,
model_description=description,
fields=_model_fields(model, set()),
)
|
Create a runtime Pydantic model from a portable definition or JSON data.
Source code in structx/schema.py
| def model_from_extraction_request(
request: ExtractionRequest | Mapping[str, Any],
) -> Type[BaseModel]:
"""Create a runtime Pydantic model from a portable definition or JSON data."""
definition = (
request
if isinstance(request, ExtractionRequest)
else ExtractionRequest.model_validate(request)
)
return ModelGenerator.from_extraction_request(definition)
|
Return the canonical, alias-free type catalog for schema-builder UIs.
Source code in structx/schema.py
| def get_type_capabilities() -> TypeCapabilities:
"""Return the canonical, alias-free type catalog for schema-builder UIs."""
return _TYPE_CAPABILITIES.model_copy(deep=True)
|
A complete extraction plan generated in one model call.
Source code in structx/core/models.py
| class ExtractionPlan(BaseModel):
"""A complete extraction plan generated in one model call."""
instructions: str = Field(description="Explicit extraction instructions")
target_columns: List[str] = Field(description="Input columns needed for extraction")
extraction_schema: ExtractionRequest = Field(alias="schema")
|
ExtractionPlan is the validated result of dynamic planning. It combines the
instructions used for row extraction, selected input columns, and the generated
schema. Supplying a custom Pydantic model skips this planning request.
ModelGenerator
Factory for generating dynamic Pydantic models.
Source code in structx/extraction/generator.py
| class ModelGenerator:
"""Factory for generating dynamic Pydantic models."""
@staticmethod
def _nested_field_type(field: ModelField, nested_model: Type[BaseModel]) -> type:
expression = normalize_type_expression(field.type)
is_optional = expression.startswith("Optional[")
if is_optional:
expression = expression[len("Optional[") : -1]
if expression.startswith("List["):
field_type = List[nested_model]
elif expression.startswith("Set["):
field_type = Set[nested_model]
elif expression.startswith("FrozenSet["):
field_type = FrozenSet[nested_model]
elif expression.startswith("Tuple["):
field_type = Tuple[nested_model]
else:
field_type = nested_model
return Optional[field_type] if is_optional else field_type
@staticmethod
def _field_annotation(field: ModelField, field_type: type) -> type:
type_is_nullable = type(None) in get_args(field_type)
if field.nullable and not type_is_nullable:
return Optional[field_type]
return field_type
@staticmethod
def _field_default(field: ModelField):
if field.required:
return ...
if field.has_default:
return field.default
return None
@classmethod
def _create_nested_model(
cls,
field_name: str,
field_description: str,
nested_fields: List[ModelField],
parent_name: str = "",
frozen: bool = False,
) -> Type[BaseModel]:
"""Create a nested Pydantic model"""
logger.debug(f"\nCreating nested model: {field_name}")
logger.debug(f"Parent name: {parent_name}")
field_definitions: Dict[str, tuple[type, Field]] = {}
for field in nested_fields:
if field.nested_fields:
nested_expression = normalize_type_expression(field.type)
nested_model = cls._create_nested_model(
field_name=field.name,
field_description=field.description,
nested_fields=field.nested_fields,
parent_name=field_name,
frozen=(
"Set[" in nested_expression or "FrozenSet[" in nested_expression
),
)
field_type = cls._nested_field_type(field, nested_model)
else:
field_type = resolve_type_expression(field.type)
field_definitions[field.name] = (
cls._field_annotation(field, field_type),
Field(
default=cls._field_default(field),
description=field.description,
**(field.validation or {}),
),
)
model_name = f"{parent_name}{field_name}" if parent_name else field_name
# Create the model using Pydantic v2 style
model_options = {"__config__": ConfigDict(frozen=frozen, validate_default=True)}
model = create_model(model_name, **model_options, **field_definitions)
# Add description as model docstring
model.__doc__ = field_description
return model
@classmethod
@handle_errors(
error_message="Error generating model from extraction request",
error_type=ModelGenerationError,
)
def from_extraction_request(cls, request: ExtractionRequest) -> Type[BaseModel]:
"""Create a new model from extraction request"""
logger.debug("Starting model generation from extraction request")
logger.debug(f"Model name: {request.model_name}")
logger.debug(f"Description: {request.model_description}")
logger.debug("Fields:")
for field in request.fields:
logger.debug(f"- {field.name}: {field.type}")
if field.nested_fields:
logger.debug(
f" With nested fields: {[f.name for f in field.nested_fields]}"
)
# Create the model
model = cls._create_nested_model(
field_name=request.model_name,
field_description=request.model_description,
nested_fields=request.fields,
)
setattr(model, "__structx_definition__", request.model_copy(deep=True))
logger.debug("Model generation completed")
return model
|
Create a new model from extraction request
Source code in structx/extraction/generator.py
| @classmethod
@handle_errors(
error_message="Error generating model from extraction request",
error_type=ModelGenerationError,
)
def from_extraction_request(cls, request: ExtractionRequest) -> Type[BaseModel]:
"""Create a new model from extraction request"""
logger.debug("Starting model generation from extraction request")
logger.debug(f"Model name: {request.model_name}")
logger.debug(f"Description: {request.model_description}")
logger.debug("Fields:")
for field in request.fields:
logger.debug(f"- {field.name}: {field.type}")
if field.nested_fields:
logger.debug(
f" With nested fields: {[f.name for f in field.nested_fields]}"
)
# Create the model
model = cls._create_nested_model(
field_name=request.model_name,
field_description=request.model_description,
nested_fields=request.fields,
)
setattr(model, "__structx_definition__", request.model_copy(deep=True))
logger.debug("Model generation completed")
return model
|
ModelGenerator.from_extraction_request() is the low-level path for turning an
ExtractionRequest into a runtime Pydantic model. Normal extraction performs
this step automatically.