StructCast
Capability reference for each module, with entry points and key APIs.
Quick Reference
Instantiation: instantiate({"_obj_": [{"_addr_": "module.Class"}]})
Data Access: construct(data, convert_spec({"key": "path.to.data"}))
Templates: extend_structure(config, template_kwargs={...})
Security: configure_security(allowed_modules={...})
Common Workflows
Workflow 1: Simple Object Instantiation from YAML
from structcast.core.instantiator import instantiate
from structcast.utils.base import load_yaml_from_string
config = """
processor:
_obj_:
- _addr_: collections.Counter
- _call_:
- [1, 2, 2, 3, 3, 3]
"""
data = load_yaml_from_string(config)
counter = instantiate(data["processor"])
# Result: Counter({3: 3, 2: 2, 1: 1})
Workflow 2: Extract and Reshape Nested Data
from structcast.core.specifier import FlexSpec
data = {
"users": [
{"profile": {"name": "Alice", "age": 30}},
{"profile": {"name": "Bob", "age": 25}}
]
}
spec = FlexSpec.model_validate({
"names": "users.*.profile.name",
"ages": "users.*.profile.age"
})
result = spec(data)
# Result: {"names": ["Alice", "Bob"], "ages": [30, 25]}
Workflow 3: Config-Driven Pipeline with Templates
from structcast.core.template import extend_structure
from structcast.core.specifier import FlexSpec
from structcast.core.instantiator import instantiate
# 1. Load config with templates
config_with_templates = {
"processor": {
"_jinja_yaml_": """
_obj_:
- _addr_: "{{ processor_class }}"
- _call_: {{ processor_kwargs | tojson }}
"""
},
"input_path": "data.{{ env }}.values"
}
# 2. Expand templates
expanded = extend_structure(config_with_templates, template_kwargs={
"default": {"processor_class": "int", "processor_kwargs": {}, "env": "prod"}
})
# 3. Extract data paths and build objects
spec = FlexSpec.model_validate({"input": expanded["input_path"]})
processor = instantiate(expanded["processor"])
# 4. Apply to data
data = {"data": {"prod": {"values": ["123", "456"]}}}
values = spec(data)["input"]
result = [processor(v) for v in values]
# Result: [123, 456]
Exceptions
Module: structcast.core.exceptions
Handle typed exceptions from each module:
| Exception | Raised By | Common Causes |
|---|---|---|
SpecError | specifier.py | Invalid spec syntax, access failures, resolver errors |
InstantiationError | instantiator.py | Pattern validation errors, recursion/timeout limits |
StructuredExtensionError | template.py | Template rendering failures, invalid extension operations |
SecurityError | utils/security.py | Blocked imports, dangerous attributes, path violations |
Object Instantiation
Module: structcast.core.instantiator
Create live Python objects from serializable dict/list patterns without writing imports or factory code.
| Capability | Entry Point | Example |
|---|---|---|
| Import by address | instantiate({"_obj_": [{"_addr_": "collections.Counter"}]}) | Returns Counter class |
| Call with kwargs | {"_call_": {"key": "val"}} inside _obj_ | Counter(key=val) |
| Call with args | {"_call_": [[1, 2, 3]]} inside _obj_ | Counter([1, 2, 3]) |
| Partial application | {"_bind_": {"base": 16}} inside _obj_ | partial(int, base=16) |
| Attribute access | {"_attr_": "real"} inside _obj_ | obj.real |
| Chain operations | {"_obj_": [addr, attr, call]} | Sequential build pipeline |
| Recursive walk | instantiate(nested_dict_with_patterns) | Resolves patterns at any depth |
| File-based import | {"_addr_": "func", "_file_": "/path/to/mod.py"} | Load from local .py file |
| Custom patterns | register_pattern(YourPattern) then use {"_your_key_": ...} | Extend with domain-specific patterns |
Key classes: BasePattern (ABC), AddressPattern, AttributePattern, CallPattern, BindPattern, ObjectPattern, PatternResult
Custom Pattern Extension:
Create domain-specific patterns by inheriting from BasePattern:
from structcast.core.instantiator import BasePattern, PatternResult, register_pattern, validate_pattern_result
from pydantic import Field
from typing import Optional
class MultiplyPattern(BasePattern):
factor: int = Field(alias="_multiply_") # Must use Field(alias=...)
def build(self, result: Optional[PatternResult] = None) -> PatternResult:
res_t, ptns, runs, depth, start = validate_pattern_result(result) # Required
if not runs:
raise InstantiationError("No value to multiply.")
runs, last = runs[:-1], runs[-1] # Extract last object from stack
new_value = last * self.factor
return res_t(patterns=ptns + [self], runs=runs + [new_value], depth=depth, start=start)
register_pattern(MultiplyPattern) # Register globally
instantiate({"_obj_": [{"_addr_": "int"}, {"_call_": ["10"]}, {"_multiply_": 3}]}) # → 30
Requirements: Inherit BasePattern, use Field(alias=...), implement build(), call validate_pattern_result(), return new PatternResult with updated patterns and runs lists. All security constraints (recursion, timeout) apply automatically.
Constraints: Max recursion depth 100, max time 30s. All imports validated by security layer.
Data Access & Reshaping
Module: structcast.core.specifier
Navigate nested data with dot-notation paths and construct new structures from specs.
| Capability | Entry Point | Example |
|---|---|---|
| Parse spec string | convert_spec("a.b.0.c") | SpecIntermediate(source, ("a","b",0,"c")) |
| Access nested path | access(data, ("a", "b", 0)) | Returns data["a"]["b"][0] |
| Construct from spec | construct(data, converted_spec) | Builds new dict from spec + data |
| Constant value | convert_spec("constant: 42") | Returns literal "42" |
| Skip entry | convert_spec("skip:") | Omits key from output |
| Placeholder (deferred) | convert_spec("placeholder: a.b") | Multi-stage resolution |
| FlexSpec (unified) | FlexSpec.model_validate({"key": "path"}) | Auto-dispatches RawSpec/ObjectSpec |
| FlexSpec with objects | FlexSpec.model_validate({"_obj_": [...]}) | Inline instantiation |
| Copy semantics | configure_spec(return_type=ReturnType.DEEP_COPY) | Control reference/copy behavior |
| Custom resolver | register_resolver("env", lambda k: os.environ.get(k)) | Extend spec language |
| Custom accessor | register_accesser(MyType, my_accessor_fn) | Access custom data types |
| BaseModel access | Enabled by default (support_basemodel=True) | Navigate Pydantic models |
| Attribute access | Enabled by default (support_attribute=True) | getattr() on objects |
| Post-processing pipe | WithPipe base, _pipe_ alias | Chain transformations after spec |
| Configure settings | configure_spec(return_type=..., support_basemodel=..., support_attribute=..., raise_error=...) | Global spec behavior |
Key classes: SpecIntermediate, RawSpec, ObjectSpec, FlexSpec, WithPipe, SpecSettings, ReturnType
FlexSpec Auto-Dispatch Rules:
- •String/int/float/bool values →
RawSpec(dot-path navigation) - •Dicts with
_obj_key →ObjectSpec(instantiation) - •Dicts without patterns → recursive
FlexSpecon values - •Lists/tuples → recursive
FlexSpecon elements - •Explicit
_spec_key → manual control over dispatch
Pipe Usage Pattern:
# Extract data then apply transformations
spec = FlexSpec.model_validate({
"sorted_users": {
"_spec_": "data.users",
"_pipe_": [
{"_obj_": [{"_addr_": "sorted"}, {"_call_": {"key": {"_obj_": [{"_addr_": "operator.itemgetter"}, {"_call_": ["name"]}]}}}]},
{"_obj_": [{"_addr_": "list"}, {"_call_": []}]}
]
}
})
# Common pipe patterns:
# - Type casting: {"_obj_": [{"_addr_": "int"}, {"_call_": []}]}
# - Sorting: {"_obj_": [{"_addr_": "sorted"}, {"_call_": {}}]}
# - Filtering: {"_obj_": [{"_addr_": "filter"}, {"_bind_": {"function": ...}}, {"_call_": []}]}
Template Rendering
Module: structcast.core.template
Embed Jinja2 templates inside data structures for dynamic configuration generation.
| Capability | Entry Point | Example |
|---|---|---|
| Render string | JinjaTemplate.model_validate({"_jinja_": "Hello {{ name }}"}) | Returns rendered string |
| Render → YAML | JinjaYamlTemplate.model_validate({"_jinja_yaml_": yaml_str}) | Renders then parses YAML |
| Render → JSON | JinjaJsonTemplate.model_validate({"_jinja_json_": json_str}) | Renders then parses JSON |
| Expand entire structure | extend_structure(data, template_kwargs={...}) | Recursive template resolution |
| Template groups | {"_jinja_group_": "group_name"} alongside template | Scoped variable sets |
| Mapping pattern | _jinja_yaml_ as key in dict → merge into parent | Dynamic key injection |
| Sequence pattern | {"_jinja_yaml_": ...} in list → splice into list | Dynamic list expansion |
| Post-processing | _jinja_pipe_ field | Chain ObjectPatterns after render |
| Sandboxed execution | ImmutableSandboxedEnvironment (default) | Safe by default |
| Configure environment | configure_jinja(environment_type=..., undefined_type=..., trim_blocks=..., lstrip_blocks=..., extensions=...) | Override Jinja settings |
Key classes: JinjaTemplate, JinjaYamlTemplate, JinjaJsonTemplate, JinjaSettings
JinjaSettings Configuration:
- •
environment_type: Jinja environment class (default:ImmutableSandboxedEnvironment) - •
undefined_type: How to handle undefined variables (default:StrictUndefined) - •
trim_blocks: Remove first newline after block (default:True) - •
lstrip_blocks: Strip leading spaces before blocks (default:True) - •
extensions: List of Jinja extensions (default:["jinja2.ext.loopcontrols"])
Security Enforcement
Module: structcast.utils.security
Validate all dynamic imports, attribute access, and file paths.
| Capability | Entry Point | Example |
|---|---|---|
| Configure security | configure_security(blocked_modules=..., allowed_modules=...) | Global settings |
| Block modules | DEFAULT_BLOCKED_MODULES | os, subprocess, pickle, etc. |
| Allow modules | DEFAULT_ALLOWED_MODULES | builtins.int, math.sqrt, etc. |
| Block dunders | DEFAULT_DANGEROUS_DUNDERS | __subclasses__, __globals__, etc. |
| Validate import | validate_import(module_name, target) | Checks blocklist + allowlist |
| Validate attribute | validate_attribute("obj.method") | Checks dunders + protected/private |
| Check file path | check_path(path) | Hidden dir + working dir checks |
| Register import dir | register_dir(path) / unregister_dir(path) | Extend allowed file search paths |
Key classes: SecuritySettings, SecurityError
SecuritySettings Fields:
- •
blocked_modules: set[str]— Module names to block (default:DEFAULT_BLOCKED_MODULES) - •
allowed_modules: dict[str, Optional[set[Optional[str]]]]— Allowlist with specific members- •
Nonevalue → disable allowlist for that module - •
setvalue → only listed members allowed - •
Nonein set → all members allowed
- •
- •
dangerous_dunders: set[str]— Blocked dunder attributes (default:DEFAULT_DANGEROUS_DUNDERS) - •
ascii_check: bool— Block non-ASCII attribute names (default:True) - •
protected_member_check: bool— Block_protectedmembers (default:True) - •
private_member_check: bool— Block__privatemembers (default:True) - •
hidden_check: bool— Block paths with hidden directories (default:True) - •
working_dir_check: bool— Enforce working directory containment (default:True)
YAML Operations
Module: structcast.utils.base (delegates to security.py)
| Capability | Entry Point |
|---|---|
| Load YAML file | load_yaml(path) — path-validated, uses ruamel.yaml |
| Load YAML string | load_yaml_from_string(yaml_str) — safe YAML parsing |
| Dump to YAML file | dump_yaml(data, path) — path-validated, preserves formatting |
| Dump to YAML string | dump_yaml_to_string(data) — serialize to YAML string |
| Security-checked import | import_from_address(addr) — validates then imports module/attr |
| Normalize to list | check_elements(value) — None→[], scalar→[scalar], seq→list |
| Unroll call arguments | unroll_call(args, kwargs) — normalize to (*args, **kwargs) tuple |
Integration Pipeline
Pattern: Combine all modules into an end-to-end config-driven workflow.
load_yaml_from_string → extend_structure → FlexSpec → instantiate → process → JinjaTemplate
| Phase | Module | Function |
|---|---|---|
| Parse | utils/base | load_yaml_from_string(yaml) |
| Expand | core/template | extend_structure(data, template_kwargs={"default": params}) |
| Extract | core/specifier | FlexSpec.model_validate(spec)(expanded) |
| Build | core/instantiator | instantiate(obj_pattern) |
| Process | (user code) | Apply built objects to extracted data |
| Report | core/template | JinjaTemplate.model_validate({...})(**data) |
Signature pattern — two-stage FlexSpec:
- •First FlexSpec reads config to extract dot-path strings
- •Second FlexSpec uses those extracted paths as specs against a different data source
See examples/06_sensor_dashboard.py, examples/07_validation_pipeline.py, examples/08_multi_tenant_analytics.py.
Troubleshooting
Common Errors
SecurityError: Module 'X' is blocked
- •Cause: Importing from a blocked module (e.g.,
os,subprocess) - •Solution: Either use an allowed alternative or configure security with
configure_security(blocked_modules=...)
InstantiationError: Maximum recursion depth exceeded
- •Cause: Circular references or deeply nested patterns (depth > 100)
- •Solution: Flatten your configuration or check for circular pattern references
SpecError: Key (X) not found in mapping
- •Cause: Dot-path accessing non-existent key
- •Solution: Use
configure_spec(raise_error=False)to returnNoneinstead, or validate data shape first
ValidationError when creating patterns
- •Cause: Using direct constructor instead of
model_validate() - •Solution: Always use
Pattern.model_validate(data)notPattern(**data)
Template variables undefined
- •Cause: Missing variables in
template_kwargswithStrictUndefined - •Solution: Provide all variables or configure with
configure_jinja(undefined_type=Undefined)