Skip to content

Validator lets a name-matching builder fall through to a wildcard spec instead of checking the named typed spec #1554

Description

@rly

What happened?

When a group spec has an optional named sub-spec with a data type (name: col1, data_type_inc: TypedVector, quantity: '?') alongside a sibling wildcard sub-spec of the parent type (data_type_inc: BaseVector, quantity: '*'), a builder named col1 whose type is only BaseVector is silently absorbed by the wildcard spec and validates clean.

SpecMatcher._best_matching_spec filters candidate specs by name and then by type (validator.py:798-799). _filter_by_type drops the named col1 spec because the builder's type is not a subtype of TypedVector, leaving only the wildcard, so the name match is discarded rather than treated as authoritative. The named spec then has zero matched builders, and since it is optional, __validate_presence_and_quantity reports nothing.

A required named sub-spec is caught, because zero matches on a required spec yields MissingDataType. So the enforcement of a named typed sub-spec currently depends entirely on whether it is required.

Steps to Reproduce

from hdmf.build import DatasetBuilder, GroupBuilder
from hdmf.spec import DatasetSpec, GroupSpec, SpecCatalog, SpecNamespace
from hdmf.validate import ValidatorMap

base = DatasetSpec(doc="base vector", data_type_def="BaseVector", dtype="int")
typed = DatasetSpec(doc="typed vector", data_type_def="TypedVector", data_type_inc="BaseVector")
container = GroupSpec(
    doc="holds vectors",
    data_type_def="Container",
    datasets=[
        DatasetSpec(doc="any number of vectors", data_type_inc="BaseVector", quantity="*"),
        DatasetSpec(doc="optional named typed vector", name="col1", data_type_inc="TypedVector", quantity="?"),
    ],
)

catalog = SpecCatalog()
for spec in (base, typed, container):
    catalog.register_spec(spec, "test.yaml")
namespace = SpecNamespace("test namespace", "test_ns", [{"source": "test.yaml"}], version="0.1.0", catalog=catalog)
vmap = ValidatorMap(namespace)


def dset(name, data_type):
    return DatasetBuilder(name, [1, 2, 3], attributes={"data_type": data_type, "object_id": name})


def group(*datasets):
    return GroupBuilder("c", datasets={d.name: d for d in datasets},
                        attributes={"data_type": "Container", "object_id": "c"})


print(vmap.validate(group(dset("col1", "TypedVector"))))  # [] -- correct
print(vmap.validate(group(dset("col1", "BaseVector"))))   # [] -- expected a type error
print(vmap.validate(group(dset("other", "BaseVector"))))  # [] -- correct, col1 is optional

The NWB case that prompted this: EventsTable specifies an optional duration dataset of type DurationVectorData next to DynamicTable's wildcard VectorData spec. A file whose duration column is a plain VectorData (no unit attribute, arbitrary dtype) validates with no errors. See NeurodataWithoutBorders/nwb-schema#708 and #1553.

Expected behavior

A builder whose name matches a named sub-spec is checked against that sub-spec, so a type mismatch is reported instead of the builder falling through to a wildcard sibling. One option is for _filter_by_name to short-circuit: if any candidate spec has a name equal to the builder's name, restrict the candidate set to those specs before filtering by type. The docstring for _best_matching_spec already notes that the matching algorithm is order-dependent and does not consider the inheritance hierarchy.

Operating System

macOS

Python Executable

Conda

Python Version

3.14

Package Versions

hdmf 6.1.1.dev14 (dev branch)

Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions