Skip to content

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.

A table that should become project objects
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.

A normal schema record
from talika import RowTable, field, integer


class UserRecordTable(RowTable):
    username = field("username", required=True)
    age = field("age", parser=integer())
    role = field("role", default="reader")
The authored datatable
users_table = [
    ["username", "age", "role"],
    ["alice", "34", "admin"],
]
Default parse output
>> 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.

A dataclass output model
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:

Default output-model call
User(**record.as_dict())

That means the output model receives Python values, not raw table text. In the example below, age reaches the dataclass as 34, not "34".

parse() and parse_as() side by side
source_records = UserTable.parse(users_table)
public_users = UserTable.parse_as(users_table)
Public output and schema records
>> 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.

Inspecting source metadata before output conversion
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:

users = UserTable.parse_as(users_table, User)

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.

Validation before model construction
@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.

A record rejected before output conversion
AdultUserTable.parse_as(
    [
        ["username", "age"],
        ["kai", "16"],
    ]
)
Record validation still owns this failure
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
A custom output builder
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.

Building output with parse context
DisplayUserTable.parse_as(
    [
        ["username", "age"],
        ["alice", "34"],
    ],
    context={"prefix": "QA-"},
)
Custom output
[{'label': 'QA-alice', 'adult': True}]

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.

An output model that rejects one record
@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())
Parsing a record rejected by the model
StrictUserTable.parse_as(
    [
        ["username", "age"],
        ["blocked", "22"],
    ]
)
Output failure diagnostic
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.

A Pydantic output model
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())
Pydantic output
>> 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.