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.
from talika import RowTable, field, integer
class UserTable(RowTable):
name = field("name", required=True)
age = field("Age", parser=integer())
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.
Use validate() when a tool, test, or editor needs to inspect a result value
without catching authored-data exceptions:
result = UserTable.validate(datatable)
if result.valid:
records = result.records
else:
for diagnostic in result.errors:
print(diagnostic.code, diagnostic.row, diagnostic.column)
>> 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:
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:
recordscontains schema records only when the complete table is validdiagnosticscontains errors and warnings in discovery ordererrorsandwarningsfilter that tuple without reordering itvalidis 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().
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.
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,
)
>> 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.
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,
}
>> 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.
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.diagnosticis the underlying immutableDiagnostic- legacy properties such as
schema,field,value,code, and coordinates remain available TableErrors.errorsremains an immutable tuple ofTableErrorTableErrors.diagnosticsexposes the corresponding diagnostic tupleSchemaDefinitionError.diagnosticusescode="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.
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:
- schema finalization and context normalization
- raw table validation and transformation
- shape, labels, variants, fields, and IDs
- reference indexing and resolution
- record and whole-table validation
- 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.