Output Models⚓︎
Talika schemas are excellent at parsing authored tables. They know labels, aliases, parsers, defaults, source cells, validation rules, references, and table shape.
Application code often wants something narrower. A test setup may want a dataclass, a Pydantic model, a factory payload, or a small dictionary that can be passed directly to a helper. Output models let the schema do the table work first, then convert each validated record into the public object your test code wants to use.
Feature: User setup
Scenario: Build project users from a table
Given the users:
| username | age | role |
| alice | 34 | admin |
Then the test can use project user objects
Separate parsing from public output
Let the schema own the table contract. Let the output model represent the object your tests or factories actually need after the table has been parsed and validated.
Start With Schema Records⚓︎
parse() always returns schema record objects, even when the schema configures
an output model.
from talika import RowTable, field, integer
class UserRecordTable(RowTable):
username = field("username", required=True)
age = field("age", parser=integer())
role = field("role", default="reader")
>> UserRecordTable.parse(users_table)
[UserRecordTable(username='alice', age=34, role='admin')]
>> UserRecordTable.parse(users_table)[0].as_dict()
{'username': 'alice', 'age': 34, 'role': 'admin'}
Schema records are useful. They carry parsed values and Talika metadata. They
also support helpers such as as_dict() and source_for(...).
The tradeoff is that schema records are table-facing objects. They still
belong to the parsing layer. If the rest of the test suite already uses a
User dataclass or a domain factory payload, returning schema records may
push table-specific details farther into the code than necessary.
Schema records are not wrong
Use schema records when the caller needs table source metadata, intermediate fields, or schema methods. Use output models when the caller should receive project objects.
Add a Dataclass Output Model⚓︎
Set output_model to a callable that accepts the parsed record fields as
keyword arguments. A dataclass is the simplest example.
from dataclasses import dataclass
@dataclass(frozen=True)
class User:
username: str
age: int
role: str
class UserTable(RowTable):
output_model = User
username = field("username", required=True)
age = field("age", parser=integer())
role = field("role", default="reader")
When parse_as() is called without an explicit target, Talika uses the
configured model and calls:
That means the output model receives Python values, not raw table text. In the
example below, age reaches the dataclass as 34, not "34".
source_records = UserTable.parse(users_table)
public_users = UserTable.parse_as(users_table)
>> public_users
[User(username='alice', age=34, role='admin')]
>> source_records
[UserTable(username='alice', age=34, role='admin')]
>> type(public_users[0]).__name__
'User'
>> type(source_records[0]).__name__
'UserTable'
parse() returns schema instances. parse_as() runs the same lifecycle and
then returns public objects.
Match output fields to parsed fields
The default output_model call uses every value from record.as_dict().
The model constructor must accept those names, or you should override
build_output() and choose the payload yourself.
Keep Records When You Need Source Detail⚓︎
Output objects are intentionally clean. They usually do not know about table rows, columns, original text, aliases, or Talika source cells.
Use parse() when the caller needs those details.
record = UserTable.parse(users_table)[0]
age_source = record.source_for("age")
assert record.age == 34
assert age_source.source_value == "34"
assert age_source.source_row == 2
assert age_source.source_column == 2
This is common in diagnostics, custom assertions, and helper code that needs
to explain where a table value came from. A domain object such as User should
not normally need to know that age came from row 2, column 2 of a feature
file. The schema record is the right object for that job.
Use parse for table-aware work
If the code needs source_for(...), table_source, item_id, or schema
methods, ask for records with parse(). If the code only needs clean
project data, use parse_as().
Supply an Output Model for One Call⚓︎
The output target does not have to live on the schema. Pass any callable that accepts the record fields as keyword arguments:
An explicit target takes precedence over configured base or variant output
hooks. Talika raises TypeError immediately when the supplied target is not
callable. Calling parse_as(table) without a configured conversion raises a
clear ValueError.
Validation Runs Before Output Conversion⚓︎
Output conversion happens near the end of the lifecycle. Talika first parses fields, applies defaults, resolves references, and runs validation. Only then does it build the public output object.
@dataclass(frozen=True)
class AdultUser:
username: str
age: int
class AdultUserTable(RowTable):
output_model = AdultUser
username = field("username", required=True)
age = field("age", parser=integer())
def validate_record(self, context):
if self.age < 18:
raise ValueError("user must be an adult")
If the record is invalid, the output model is not constructed.
AdultUserTable.parse_as(
[
["username", "age"],
["kai", "16"],
]
)
Record validation failed: user must be an adult (code=record_validation_failed, schema=AdultUserTable, row=2)
This keeps responsibilities clear. The schema explains table rules and record-level rules. The output model receives records that have already passed those rules.
Output models can still validate themselves
A dataclass __post_init__, Pydantic model, or factory can still reject
data. Talika reports that as an output failure because the table record had
already parsed and validated successfully.
Build Custom Output⚓︎
Use build_output() when the public object is not a direct constructor call
from every parsed field.
Common reasons include:
- the factory needs parse context
- the output should omit table-only fields
- field names differ between the table schema and the public object
- the output object should be a dictionary, command object, or factory payload
- construction requires a service or helper supplied through context
class DisplayUserTable(RowTable):
username = field("username", required=True)
age = field("age", parser=integer())
@classmethod
def build_output(cls, record, context):
prefix = context.user_data.get("prefix", "")
return {
"label": f"{prefix}{record.username}",
"adult": record.age >= 18,
}
The builder receives a validated schema record and the parse context.
DisplayUserTable.parse_as(
[
["username", "age"],
["alice", "34"],
],
context={"prefix": "QA-"},
)
This is often better than forcing the table fields to match a constructor
exactly. The schema can keep table labels readable, while build_output()
performs the small translation needed by the test setup.
Do not hide parsing rules in build_output
build_output() should convert a validated record into public output. Keep
cell parsing in field parsers and record rules in validators so failures
still point to the right table layer.
Handle Output Construction Errors⚓︎
If output construction raises an exception, Talika wraps it in a source-aware
TableError with code=output_failed.
A custom output model or builder may raise a deliberate TableError,
TableErrors, or SchemaDefinitionError; Talika preserves that exception
instead of reclassifying it. parse(), validate(), and static
checking never call output conversion.
@dataclass(frozen=True)
class StrictUser:
username: str
age: int
def __post_init__(self):
if self.username == "blocked":
raise ValueError("blocked user cannot be exported")
class StrictUserTable(RowTable):
output_model = StrictUser
username = field("username", required=True)
age = field("age", parser=integer())
StrictUserTable.parse_as(
[
["username", "age"],
["blocked", "22"],
]
)
Output model StrictUser rejected the record: blocked user cannot be exported (code=output_failed, schema=StrictUserTable, row=2)
The diagnostic points to the record location because the failure happened while building the public object for that record. For row tables, that usually means the source row. For column tables, it usually means the item column and item ID.
Use clear model errors
The exception message from the output constructor becomes part of the table diagnostic. Keep those messages useful for someone editing the authored table.
Use Pydantic as a Normal Output Model⚓︎
Pydantic models use the same output contract. The model is called with parsed field values as keyword arguments.
import pydantic
class UserModel(pydantic.BaseModel):
username: str
age: int = pydantic.Field(ge=18)
class PydanticUserTable(RowTable):
output_model = UserModel
username = field("username", required=True)
age = field("age", parser=integer())
>> PydanticUserTable.parse_as([["username", "age"], ["alice", "34"]])
[UserModel(username='alice', age=34)]
Talika does not need a separate parsing path for this. Pydantic receives the already parsed values and performs its own model validation during output construction.
Dataclass and Pydantic constructors receive schema attribute names, not
table labels. For example, full_name = field("Full name") passes
full_name=.... Use build_output() when a project model uses different
constructor names.
Use this when your test code already speaks in Pydantic models. If Pydantic is only being used to parse table strings, prefer Talika field parsers instead so source-aware table diagnostics stay close to the authored cell.
Pydantic is an output choice
A Pydantic model should represent the public object you want after table parsing. It does not replace schema fields, source-aware parsing, or table validation.
Choose the Right Return Shape⚓︎
Use schema records when the caller is still working with the table as a table:
- source-aware assertions
- custom diagnostics
- helper code that needs
source_for(...) - advanced validation or tooling
- type-checker-friendly schema instances
Use output models when the caller is ready to leave the table layer:
- domain dataclasses
- Pydantic models
- factory payloads
- application setup commands
- dictionaries for test helpers
The useful boundary is simple: parse table text into dependable schema records, then convert those records into the object shape your test code actually wants.
Keep the table boundary explicit
A good output model makes the handoff obvious. Before conversion, the data still belongs to the authored table. After conversion, it belongs to the project code using the parsed result.