Skip to content

Diagnostics And Validation Results⚓︎

Talika uses one immutable diagnostic model for runtime parsing, non-raising validation, static checking, pytest-bdd integration, and CLI JSON. This keeps a failure's code, source location, field identity, and values consistent no matter which entry point reports it.

Start with an ordinary feature table and schema. No diagnostic-specific schema configuration is required.

A table with an invalid age
Given the users exist
  | name  | Age |
  | Alice | bad |
The table contract
from talika import RowTable, field, integer


class UserTable(RowTable):
    name = field("name", required=True)
    age = field("Age", parser=integer())
The Python datatable
datatable = [
    ["name", "Age"],
    ["Alice", "bad"],
]

Choose Raising or Non-Raising Validation⚓︎

Use the raising APIs when invalid table data should stop normal test setup. A failed parse() call raises TableError, or TableErrors when collect mode finds several independent failures.

Raise when the table is invalid
records = UserTable.parse(datatable)

Use validate() when a tool, test, or editor needs to inspect a result value without catching authored-data exceptions:

Return a validation result
result = UserTable.validate(datatable)

if result.valid:
    records = result.records
else:
    for diagnostic in result.errors:
        print(diagnostic.code, diagnostic.row, diagnostic.column)
Inspecting an invalid result
>> result.valid
False

>> result.records
()

>> [(error.code, error.row, error.column) for error in result.errors]
[('parser_failed', 2, 2)]

The functional and pytest-bdd forms run the same lifecycle. They are useful when a project prefers dependency injection or explicit function calls:

Equivalent validation entry points
from talika import validate_table


result = validate_table(
    UserTable,
    datatable,
    context={"locale": "en"},
)

# Inside a pytest-bdd step, the talika fixture offers the same operation:
result = talika.validate(
    datatable,
    schema=UserTable,
    context={"locale": "en"},
)

Choose the API at the call site

A schema does not need separate raising and non-raising versions. Use parse() in ordinary setup code and validate() where diagnostics are data that another tool or assertion needs to inspect.

Understand ValidationResult⚓︎

ValidationResult[RecordType] is frozen and contains immutable tuples:

  • records contains schema records only when the complete table is valid
  • diagnostics contains errors and warnings in discovery order
  • errors and warnings filter that tuple without reordering it
  • valid is true when there are no error-severity diagnostics

Validation always uses safe collect semantics. It runs table transformation, field parsing, defaults, IDs, variants, references, and record/table validators. It deliberately skips output models and build_output().

An invalid result never contains partial records
result = UserTable.validate(
    [["name", "Age"], ["Alice", "bad"]]
)

assert not result.valid
assert result.records == ()
assert result.errors[0].code == "parser_failed"

Invalid results never expose partially parsed records. This prevents callers from accidentally using records produced before a later field, reference, or validator failed. Successful records remain mutable, matching parse().

Declaration and API errors still raise

validate() handles authored table-data diagnostics without raising. Invalid schema families, unsupported error_mode values, and other API misuse still raise because they cannot be represented as a table result.

Keep Warnings Without Invalidating Records⚓︎

Validation hooks may raise a TableError with severity=DiagnosticSeverity.WARNING. Warning-only validation remains valid and keeps its complete records.

A record validator that reports a warning
from talika import DiagnosticSeverity, TableError


class ReviewTable(RowTable):
    name = field("name", required=True)

    def validate_record(self, context):
        if self.name == "legacy":
            raise TableError(
                "Replace the legacy display name when practical",
                code="legacy_name",
                severity=DiagnosticSeverity.WARNING,
            )
Validate a warning-only table
warning_result = ReviewTable.validate(
    [["name"], ["legacy"]]
)
A valid result with one warning
>> warning_result.valid
True

>> [record.name for record in warning_result.records]
['legacy']

>> [warning.code for warning in warning_result.warnings]
['legacy_name']

validate() returns warnings in result.diagnostics and result.warnings. The raising APIs emit a public TalikaWarning through Python's warnings system and still return their data. If warnings and errors coexist, warnings remain in discovery order, no partial records are returned, and raising APIs emit the warnings before raising the error failures.

Warnings belong to validation

A parser, default factory, or transformer must produce a value for later lifecycle stages. Failures at those value-producing boundaries remain errors even if a project-created TableError requests warning severity.

Read a Diagnostic⚓︎

Diagnostic is a frozen, slotted value with diagnostic_version = 1.

Select stable diagnostic fields
diagnostic = result.diagnostics[0]

details = {
    "version": diagnostic.diagnostic_version,
    "severity": diagnostic.severity.value,
    "code": diagnostic.code,
    "schema": diagnostic.schema_name,
    "field": diagnostic.field_name,
    "label": diagnostic.field_label,
    "location": (diagnostic.row, diagnostic.column),
    "source": diagnostic.source_value,
}
Structured parser diagnostic
>> details
{'version': 1, 'severity': 'error', 'code': 'parser_failed', 'schema': 'UserTable', 'field': 'age', 'label': 'Age', 'location': (2, 2), 'source': 'bad'}

field_name identifies the Python declaration. field_label identifies the authored canonical label or alias. An unknown authored label has no field name, but is retained as the field label.

source_value is what the author wrote. logical_value is what a transformer made available to later parsing. Both may be useful when compact syntax is expanded or normalized.

Distinguish omitted values from explicit None
if diagnostic.has_source_value:
    print("authored:", diagnostic.source_value)
if diagnostic.has_logical_value:
    print("logical:", diagnostic.logical_value)

The has_item_id, has_source_value, and has_logical_value flags distinguish an omitted value from a value explicitly set to None. The public value property returns None in both cases, so inspect the presence flag when that distinction matters.

cause retains the original exception for programmatic debugging. It is excluded from equality and JSON because exceptions are not stable data. as_dict() returns deterministic JSON-compatible Model v1 fields.

Use structured data in integrations

Human-readable exception text may improve over time. Test runners, editor integrations, and other tools should use diagnostic attributes or as_dict() instead of parsing formatted messages.

Keep Raising APIs Compatible⚓︎

TableError, TableErrors, and SchemaDefinitionError remain the exceptions used by existing applications. The shared diagnostic model sits underneath these exceptions; adopting structured diagnostics does not require rewriting normal fail-fast parsing code.

  • TableError.diagnostic is the underlying immutable Diagnostic
  • legacy properties such as schema, field, value, code, and coordinates remain available
  • TableErrors.errors remains an immutable tuple of TableError
  • TableErrors.diagnostics exposes the corresponding diagnostic tuple
  • SchemaDefinitionError.diagnostic uses code="schema_definition"

Formatted exception strings may gain source and explicit field information. Integrations should consume structured properties instead of parsing text. Application code can continue catching the public exception types, while tooling reads their diagnostic or diagnostics properties.

Raise Deliberate Project Diagnostics⚓︎

At every user extension boundary, a deliberate TableError, TableErrors, or SchemaDefinitionError passes through unchanged. This applies to parsers, default factories, transformers, reference-key parsers, validators, and output builders.

A project-owned parser diagnostic
from talika import TableError


def project_code(value, context):
    if not value.startswith("USR-"):
        raise TableError(
            "User code must start with USR-",
            code="project_user_code",
            schema=context.schema,
            field_name=context.field_name,
            field_label=context.field_label,
            source_uri=context.source_uri,
            row=context.row,
            column=context.column,
            source_value=context.source_value,
        )
    return value


class ProjectUserTable(RowTable):
    code = field("Code", parser=project_code)

Raise an ordinary exception when Talika should classify the extension point: parser exceptions become parser_failed, default-factory exceptions become default_factory_failed, and so on. The wrapper retains the original cause.

Unexpected exceptions escaping Talika's own lifecycle become internal_error with a bug-report hint. Control-flow exceptions such as KeyboardInterrupt and SystemExit are never intercepted.

Respect Lifecycle Barriers⚓︎

Diagnostics remain in discovery order; Talika does not sort them. It collects independent failures within a safe phase, then stops before dependent work:

  1. schema finalization and context normalization
  2. raw table validation and transformation
  3. shape, labels, variants, fields, and IDs
  4. reference indexing and resolution
  5. record and whole-table validation
  6. output conversion for parse_as() only

Structure or conversion errors stop references and validation. Reference errors stop validators and output. Validation errors stop output. These barriers avoid secondary diagnostics produced from incomplete data.

Diagnostic Code Catalog⚓︎

Talika-owned failures always use an explicit code. table_error is reserved as the default for project-created TableError values.

Code Lifecycle owner Meaning
table_error Project extensions Default code for a user-created custom error.
schema_definition Schema compiler/family finalization A schema declaration, inheritance contract, variant family, or reference contract is invalid.
invalid_context Context normalization The supplied parse context cannot be normalized.
invalid_table_input Table boundary Raw rows, cells, coordinates, or TableData are malformed.
table_empty Table boundary/transformation A table or grouped table has no usable cells.
header_empty Orientation A required row or column header is blank.
ragged_row Orientation/transformation Rows do not form the required rectangular shape.
duplicate_label Label validation A label or canonical-label/alias combination appears more than once.
unknown_field Label validation Authored text names a field the schema does not accept.
missing_required Field conversion A required field is absent.
empty_required Field conversion A required authored cell is blank.
empty_optional Field conversion An optional cell violates its empty-value policy.
default_factory_failed Field conversion A default factory raised a non-Talika exception.
parser_failed Field conversion A field parser raised a non-Talika exception.
transform_failed Transformation A table transformer or range/repeat rule raised unexpectedly.
invalid_transform Transformation A transformer or compact expansion returned an invalid structure.
expansion_limit Group expansion Numeric expansion would create more than 10,000 keys.
unknown_variant Variant selection A discriminator value has no registered variant.
inapplicable_field Variant selection Authored data populates a field unavailable on the selected variant.
duplicate_id Identity Two parsed records use the same ID.
invalid_id Identity/orientation An ID is unhashable or a column ID row has an invalid layout.
reference_failed References Target indexing, key conversion, uniqueness, or lookup failed.
record_validation_failed Validation A record validator raised a non-Talika exception.
table_validation_failed Validation A whole-table validator raised a non-Talika exception.
output_failed Output conversion An output model or custom builder rejected a valid record.
checker_failed Static checker/CLI Feature discovery, import, Gherkin, or context setup failed operationally.
internal_error Lifecycle boundary An unexpected Talika implementation failure escaped its owning stage.

Code meanings are part of Diagnostic Model v1. New fields and codes may be added compatibly; removing a field or changing a code's meaning requires a new diagnostic or format version.

Choose An API⚓︎

Choose the narrowest return type the caller needs. Runtime setup usually wants records or output objects immediately, while checkers and editor integrations need diagnostics they can retain, filter, and serialize.

  • Use parse() for raising validation that returns schema records.
  • Use parse_as() for explicit or configured output conversion.
  • Use validate() for non-raising tooling and complete-table acceptance.
  • Use static checking for feature-file discovery plus validate().
  • Use CLI JSON when another process needs versioned deterministic data.

All of these entry points share the compiled schema and diagnostic lifecycle.

Keep one schema contract

Switching entry points changes how results are delivered, not what the table means. Reuse the same schema for runtime parsing, non-raising checks, pytest-bdd steps, and CLI validation.