Skip to content

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.

ExtractionResult

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__()

attempted_count property

Number of input rows submitted for extraction.

empty_count property

Number of successful input rows that produced no model instances.

extracted_count property

Number of extracted model instances or result rows returned.

failed property

Failed row details as a compatibility-friendly DataFrame view.

failure_count property

Number of input rows that failed extraction.

success_count property

Number of input rows extracted successfully.

success_rate property

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.

ExtractionRequest

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)

ExtractionPlan

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

from_extraction_request(request) classmethod

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.