Skip to content

Inapplicable Fields⚓︎

Variant tables often contain the union of every possible field in the table family. That shape is useful for authors because they can compare records in one place, but it also creates a question: what should happen when a record contains a value for a field that belongs to another variant?

A poll with a value in an article-only field
Feature: CMS variant cleanup

  Scenario: A poll row contains an old article-only value
    Given the content items:
      | IDs      | P-1             |
      | Type     | Poll            |
      | Headline | Reader question |
      | Body     | copied note     |
      | Options  | Yes, No         |
    Then the table policy decides whether the Body value is allowed

In this table, Body belongs to Article. The selected record is a Poll. The Options value is correct, but Body contains copied note. Talika calls that an inapplicable field: the label is known to the table family, but it does not apply to the selected variant.

Known label, wrong selected shape

An unknown field is a label the schema family does not know. An inapplicable field is a known label with a non-empty value on a record whose selected variant does not declare that field.

Start With a Variant Table⚓︎

The examples below use a small CMS table with two variants.

Shared variant fields
from talika import (
    ColumnTable,
    RowTable,
    TableFields,
    discriminator,
    field,
    id_field,
    split,
)


class ArticleFields(TableFields):
    body = field("Body", required=True)


class PollFields(TableFields):
    options = field("Options", required=True, parser=split(","))

ArticleFields owns Body. PollFields owns Options. The base table owns the shared fields.

Strict content table
class StrictContentTable(ColumnTable):
    id = id_field("IDs")
    content_type = discriminator(
        "Type",
        variants={"Article": ArticleFields, "Poll": PollFields},
    )
    headline = field("Headline", required=True)

The default policy is inapplicable_fields = "forbid". You normally do not write that line because it is already the schema default.

This policy only matters for variants

A non-variant table does not have selected and non-selected shapes. The inapplicable-field policy is only used when a discriminator has selected a concrete variant for a record.

The Default Is Forbid⚓︎

The strict default protects the table author from quiet mistakes.

A poll with a non-empty Body cell
poll_with_body = [
    ["IDs", "P-1"],
    ["Type", "Poll"],
    ["Headline", "Reader question"],
    ["Body", "copied note"],
    ["Options", "Yes, No"],
]
Parsing with the default policy
StrictContentTable.parse(poll_with_body)
Inapplicable-field diagnostic
Field does not apply to variant 'Poll' (code=inapplicable_field, schema=StrictContentTable[Poll], field='Body', row=4, column=2, item_id='P-1', value='copied note'). Hint: Move this value to a record with the matching variant, leave the cell empty, or change inapplicable_fields policy.

This failure is intentional. Body is a valid label somewhere in the table family, so Talika does not treat it as unknown. But after Type selects Poll, a non-empty Body cell looks like misplaced data.

The diagnostic points to the exact cell that should be cleared, moved, or explained by changing policy.

Forbid is the safest default

In most test data, a non-empty value in the wrong variant field is a bug. Leaving it in the table can make the scenario look like it is testing one thing while the parser silently ignores another.

Empty Cells Are Allowed⚓︎

A variant table can still include rows or columns for other variants. Empty cells are ignored.

A clean poll row
clean_poll = [
    ["IDs", "P-1"],
    ["Type", "Poll"],
    ["Headline", "Reader question"],
    ["Body", ""],
    ["Options", "Yes, No"],
]
Empty inapplicable cells are ignored
>> StrictContentTable.parse(clean_poll)
[StrictContentTablePollFieldsVariant(options=['Yes', 'No'], id='P-1', content_type='Poll', headline='Reader question')]

This is the normal union-table pattern. The table can show both Body and Options labels, while each record only fills the fields that match its variant.

Use blank cells to keep the table rectangular

Authors can leave fields blank where they do not apply. The strict policy only rejects non-empty inapplicable values.

Preserve Inapplicable Values⚓︎

Use inapplicable_fields = "preserve" when old or imported tables may contain values that do not apply to the selected variant, but you still want to inspect them.

Preserving inapplicable values
class PreserveContentTable(ColumnTable):
    inapplicable_fields = "preserve"

    id = id_field("IDs")
    content_type = discriminator(
        "Type",
        variants={"Article": ArticleFields, "Poll": PollFields},
    )
    headline = field("Headline", required=True)

With this policy, Talika does not attach the value to the selected variant as a normal field. The record still remains a Poll record. The inapplicable value is stored separately in table_extras.

Parsing with preserve
poll = PreserveContentTable.parse(poll_with_body)[0]
Preserved table extras
>> poll
PreserveContentTablePollFieldsVariant(options=['Yes', 'No'], id='P-1', content_type='Poll', headline='Reader question')

>> poll.options
['Yes', 'No']

>> dict(poll.table_extras)
{'Body': 'copied note'}

This gives migration or linting code a way to see the value without pretending that Body is part of the poll schema.

Preserve does not make the field applicable

The selected record still only has fields declared by its variant and base schema. Preserved values are extra source data, not parsed domain fields.

Extras Stay Out of as_dict⚓︎

table_extras is intentionally separate from parsed schema fields.

Extras are not schema fields
>> poll.as_dict()
{'options': ['Yes', 'No'], 'id': 'P-1', 'content_type': 'Poll', 'headline': 'Reader question'}

That matters for output models. The default output-model conversion uses record.as_dict(), so preserved inapplicable values do not get passed into a dataclass, Pydantic model, or ordinary constructor by accident.

If output conversion needs extras, use a custom build_output() that deliberately reads record.table_extras.

Do not depend on extras as required data

Extras are preserved because the value did not fit the selected variant. Treat them as migration, diagnostics, or audit data, not as part of the normal record contract.

Extras Are Read-Only⚓︎

table_extras is a read-only mapping on the parsed record.

Trying to mutate extras
poll.table_extras["Body"] = "changed"
Read-only extras error
TypeError: 'mappingproxy' object does not support item assignment

The record represents the parsed result of one authored table. If migration code needs to rewrite old values, rewrite the source table or build a new output object rather than mutating the parsed record in place.

Keep parsed records stable

Treat schema records as immutable parse results for practical purposes. They expose source data and extras for inspection, not as a place to repair the table after parsing. Declared record attributes are technically assignable in Talika 0.3; source metadata and extras are enforced as read-only.

Empty Inapplicable Cells Are Not Extras⚓︎

Preserve mode only records non-empty values. Empty cells for other variants are ignored, just as they are in strict mode.

Blank wrong-variant cells
>> clean = PreserveContentTable.parse(clean_poll)[0]
>> dict(clean.table_extras)
{}

This keeps table_extras focused. It reports authored values that may need attention, not every blank cell required to keep a union-shaped table rectangular.

Extras mean something was written

If a label appears in the table but the selected variant leaves the cell blank, Talika treats that as intentional table shape. It is not preserved as an extra.

Extras Use Authored Labels⚓︎

Preserved extras are keyed by the actual label used in the table, not by the Python field name.

A variant field with an alias
class AliasArticleFields(TableFields):
    body = field("Body", aliases=("Article body",))


class AliasPollFields(TableFields):
    options = field("Options")


class AliasContentTable(RowTable):
    inapplicable_fields = "preserve"
    kind = discriminator(
        "type",
        variants={"Article": AliasArticleFields, "Poll": AliasPollFields},
    )
Authored label in extras
>> poll = AliasContentTable.parse(
...     [["type", "Article body", "Options"], ["Poll", "legacy", "Yes, No"]]
... )[0]
>> dict(poll.table_extras)
{'Article body': 'legacy'}

The schema field name is body, and the canonical label is Body, but the table author used the alias Article body. That exact label appears in table_extras.

This is useful for migration tooling because it preserves the vocabulary that actually appeared in the authored table.

Use extras for reporting

When showing preserved values back to an author, the authored label is usually more helpful than the Python attribute name.

Unsupported Policies Fail Early⚓︎

Only two policies are supported:

  • "forbid"
  • "preserve"

"ignore" is not supported. A schema that asks for it fails during class creation.

Unsupported inapplicable-field policy
class InvalidTable(RowTable):
    inapplicable_fields = "ignore"
    value = field("value")
Schema definition error
inapplicable_fields must be 'forbid' or 'preserve' (schema=InvalidTable)

Silent ignore is deliberately absent. If a value is written into a field that does not apply to the selected variant, the parser should either reject it or preserve it for explicit inspection.

Avoid silent data loss

"preserve" is a conscious migration choice. "forbid" is the safer default for tests. A silent ignore policy would make authored values disappear without either behavior.

Choose the Policy⚓︎

Use the default "forbid" when feature tables are expected to be clean and the team wants mistakes to fail immediately.

Use "preserve" when the table is transitional:

  • old feature files still contain values from an earlier schema
  • imported data needs to be audited before cleanup
  • a tooling pass should report extra values without blocking parsing
  • migration code needs to compare selected fields and preserved source values

In most test suites, keep strict mode for normal scenario setup. Reach for preserve mode when the documentation, migration, or tooling story explicitly needs to keep those extra authored values visible.

Preserve temporarily, then remove

If preserve mode is only helping a schema migration, treat it as temporary. Once old tables are cleaned up, returning to strict mode gives better protection against misplaced data.