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
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 namedcol1whose type is onlyBaseVectoris silently absorbed by the wildcard spec and validates clean.SpecMatcher._best_matching_specfilters candidate specs by name and then by type (validator.py:798-799)._filter_by_typedrops the namedcol1spec because the builder's type is not a subtype ofTypedVector, 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_quantityreports 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
The NWB case that prompted this:
EventsTablespecifies an optionaldurationdataset of typeDurationVectorDatanext toDynamicTable's wildcardVectorDataspec. A file whosedurationcolumn is a plainVectorData(nounitattribute, 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_nameto 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_specalready 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