Checks
This page contains lists all validation checks available in data-contract-checker.
Attribute paths
Checks are applied to attributes identified by dot-notation paths, e.g. info.title or models.order.fields.id.description.
Use * as a wildcard segment to match every repeated item at that level. Contract standards represent repeatable objects differently, and * handles both:
- Dicts keyed by name (DCS style):
* expands over the dict entries.
- Lists of named objects (ODCS style):
* expands over the list items. Each concrete path is labeled with the item's name when it has one, falling back to the item's index. When two items would get the same label (for example duplicate names), each is labeled label[index], e.g. schema.orders.properties.id[0] and schema.orders.properties.id[2], so every item is checked on its own.
data_contract_checks:
# DCS: checks every model's fields collection
- attribute: models.*.fields
checks:
- min_items: 1
# DCS: checks the description of every field in every model
- attribute: models.*.fields.*.description
checks:
- required: true
# ODCS: checks the logical type of every property in every schema object
- attribute: schema.*.properties.*.logicalType
checks:
- required: true
Wildcards can appear at any level and can be combined freely within the same path.
Resolving name. A name leaf directly after a * works for both styles: in list-style contracts it reads the item's explicit name value, and in dict-style contracts — where the item's name is the key itself — it resolves to the key (an explicit name value inside the entry wins over the key). This only applies to entries that are objects: a null or scalar entry (e.g. fields: {id: null} or info.title) has no name and is reported as missing. So models.*.fields.*.name yields field names regardless of the standard.
Lists without wildcards. A non-wildcard segment applied to a list is transparent: it applies to each item, and the matched values are merged under a single path (e.g. servers.type collects the type of every server). Use * when you want checks to run per item instead.
Missing vs null values
All checks follow the same rule for attributes that are absent or empty:
- Absent key: skipped by every check except
required. The check produces no result, so a field can be present in some contracts and not in others.
- Key present but null (
owner: with no value, owner: null, or owner: ~): fails every check with Attribute '<path>' is null.
- Null elements inside a value (
tags: [a, null], or a null entry in a fields mapping): fail with Attribute '<path>' contains N null element(s). Null elements are not counted by min_items / max_items.
| Contract |
required: true |
Any other check |
info: {title: X} (no owner) |
FAIL: is missing |
no result |
info: {owner: null} |
FAIL: is null |
FAIL: is null |
info: {owner: alice} |
PASS |
checked normally |
To make a field mandatory, add required: true. The other checks never enforce presence:
data_contract_checks:
# Optional: if tags are present, there must be at least one
- attribute: info.tags
checks:
- min_items: 1
# Mandatory: tags must be present and have at least one item
- attribute: info.tags
checks:
- required: true
- min_items: 1
required: false produces no result. You can still write it to document that a field is optional.
A null parent (e.g. info: null) makes every path below it absent, so info.owner is reported as missing by required and skipped by the other checks. Add a check on info itself to report it as null.
Empty values ("", [], {}) are not null. They count as present.
See Pass and fail examples for every check run against real contracts.
Available checks
Initialize validators package and provide convenience exports.
Classes:
| Name |
Description |
AcceptedValuesCheck |
Check if attribute values are within a list of accepted values.
|
AtLeastOneCheck |
Check that at least one item in a collection satisfies a key-value condition.
|
ExactlyOneCheck |
Check that exactly one item in a collection satisfies a key-value condition.
|
MaxItemsCheck |
Check if a list attribute does not exceed a maximum number of items.
|
MinItemsCheck |
Check if a list attribute has at least a minimum number of items.
|
PatternCheck |
Check if attribute values match a regular expression pattern.
|
RequiredCheck |
Check if a required field exists and is not null.
|
AcceptedValuesCheck
Check if attribute values are within a list of accepted values.
Both string and list attributes are supported.
- For string attributes, the value is checked directly against the accepted values.
- For list attributes, each item in the list is checked against the accepted values,
and a validation result is generated for each item.
Values are compared with their type, the same way --lint checks them
against the contract schema: a contract value 255 matches an accepted
value of 255 (or 255.0) but not "255", and true matches
true but not "true" or 1. Unquoted dates are strings on both
sides, so 2024-01-01 matches 2024-01-01 and "2024-01-01".
The identifier for this check type ("accepted_values").
Example configuration in data-contract-checks.yml:
data_contract_checks:
# Single value - ensure version is accepted
- attribute: info.version
checks:
- accepted_values: 1.0.0
# Multiple accepted values for tags (list attribute)
- attribute: tags
checks:
- accepted_values:
- production
- staging
- development
# Ensure server types are valid
- attribute: servers.type
checks:
- accepted_values:
- s3
- gcs
- azure_blob
Source code in src/contract_checker/checks/validators/accepted_values_check.py
| class AcceptedValuesCheck(Check):
"""Check if attribute values are within a list of accepted values.
Both string and list attributes are supported.
- For string attributes, the value is checked directly against the accepted values.
- For list attributes, each item in the list is checked against the accepted values,
and a validation result is generated for each item.
Values are compared with their type, the same way ``--lint`` checks them
against the contract schema: a contract value ``255`` matches an accepted
value of ``255`` (or ``255.0``) but not ``"255"``, and ``true`` matches
``true`` but not ``"true"`` or ``1``. Unquoted dates are strings on both
sides, so ``2024-01-01`` matches ``2024-01-01`` and ``"2024-01-01"``.
The identifier for this check type (``"accepted_values"``).
Example configuration in ``data-contract-checks.yml``:
```yaml
data_contract_checks:
# Single value - ensure version is accepted
- attribute: info.version
checks:
- accepted_values: 1.0.0
# Multiple accepted values for tags (list attribute)
- attribute: tags
checks:
- accepted_values:
- production
- staging
- development
# Ensure server types are valid
- attribute: servers.type
checks:
- accepted_values:
- s3
- gcs
- azure_blob
```
"""
@property
def check_type(self) -> str:
"""Return the check type identifier.
:return: The string ``"accepted_values"``.
"""
return "accepted_values"
def validate_config(self, check_value: Any) -> None: # noqa: ANN401
"""Accept a non-empty list of scalar values, or a single scalar value.
Scalars are strings, numbers and booleans.
:raises ValueError: If ``check_value`` is a mapping, null or an empty
list, or a list containing a non-scalar entry (a nested list, a
mapping or null).
"""
if isinstance(check_value, _SCALAR_TYPES):
return
if not isinstance(check_value, list) or not check_value:
raise ValueError(f"'accepted_values' expects a non-empty list of values, got {check_value!r}")
for entry in check_value:
if not isinstance(entry, _SCALAR_TYPES):
raise ValueError(f"'accepted_values' entries must be strings, numbers or booleans, got {entry!r}")
def execute(
self,
attribute_path: str,
value_list: List[Any],
accepted_values: Union[_Scalar, Sequence[_Scalar]],
) -> List[ValidationResult]:
"""Execute the accepted values validation.
:param attribute_path: The dot-notation path to the attribute being checked.
:param value_list: List of values found at the attribute path. Each value
can be a string or a list of strings.
:param accepted_values: The values that are considered valid: a list of
scalars, or a single scalar as shorthand for a one-item list.
:return: A list of :class:`ValidationResult` objects. When the attribute
contains a list, a result is generated for each item in that list.
Null elements in a list fail. An absent attribute (empty
``value_list``) produces no results. Null values are reported by
the validator.
"""
# validate_config allows a single scalar as shorthand for a one-item
# list. Wrapping it keeps the test a membership test, not a substring
# test ('act' must not pass against accepted_values: active).
if isinstance(accepted_values, _SCALAR_TYPES):
accepted_values = [accepted_values]
results = []
for value in value_list:
if isinstance(value, str):
items = [value]
elif isinstance(value, list):
# Filter out null elements and report them explicitly
items = [v for v in value if v is not None]
null_count = len(value) - len(items)
if null_count:
results.append(self.null_elements_result(attribute_path, null_count))
else:
items = [value]
for item in items:
passed = any(_same_value(item, accepted) for accepted in accepted_values)
if passed:
message = f"Attribute '{attribute_path}' value '{item}' is in the list of accepted values"
else:
message = (
f"Attribute '{attribute_path}' value '{item}' "
f"is not in the list of accepted values: '{accepted_values}'"
) + self._type_mismatch_hint(item, accepted_values)
results.append(ValidationResult("", attribute_path, self.check_type, passed, message))
return results
@staticmethod
def _type_mismatch_hint(item: Any, accepted_values: Sequence[_Scalar]) -> str: # noqa: ANN401
"""Explain a failure where the value is listed, but with another type.
A contract ``255`` against ``accepted_values: ["255"]``, or ``true``
against ``["true"]``, reads as the same value but is not: point at the
quotes in the checks config rather than leave the failure unexplained.
"""
for accepted in accepted_values:
if str(accepted).lower() == str(item).lower():
return (
f" (type mismatch: the contract has {_json_type_name(item)} {item!r}, the checks config has "
f"{_json_type_name(accepted)} {accepted!r}; add or remove the quotes in the checks config)"
)
return ""
|
AtLeastOneCheck
Check that at least one item in a collection satisfies a key-value condition.
Use this check when a constraint must hold for some (not all) items.
Point the attribute path at the collection, not a leaf field, and
provide a single attr: val pair under at_least_one as a YAML block
mapping to match a nested attribute.
Example — at least one field per model must have unique: true:
data_contract_checks:
- attribute: models.*.fields
checks:
- at_least_one:
unique: true
Example — at least one field must be a primary key:
data_contract_checks:
- attribute: models.*.fields
checks:
- at_least_one:
primaryKey: true
Per-expansion semantics. When the attribute path contains a wildcard
(e.g. models.*.fields), the check runs once per expanded concrete path
— once per model in the examples above. Each expansion must independently
satisfy the condition; results are not aggregated across matches. Use a
non-wildcarded path if you want a single pooled verdict instead.
Source code in src/contract_checker/checks/validators/at_least_one_check.py
| class AtLeastOneCheck(Check):
"""Check that at least one item in a collection satisfies a key-value condition.
Use this check when a constraint must hold for *some* (not all) items.
Point the ``attribute`` path at the **collection**, not a leaf field, and
provide a single ``attr: val`` pair under ``at_least_one`` as a YAML block
mapping to match a nested attribute.
Example — at least one field per model must have ``unique: true``:
```yaml
data_contract_checks:
- attribute: models.*.fields
checks:
- at_least_one:
unique: true
```
Example — at least one field must be a primary key:
```yaml
data_contract_checks:
- attribute: models.*.fields
checks:
- at_least_one:
primaryKey: true
```
**Per-expansion semantics.** When the attribute path contains a wildcard
(e.g. ``models.*.fields``), the check runs once per expanded concrete path
— once per model in the examples above. Each expansion must independently
satisfy the condition; results are *not* aggregated across matches. Use a
non-wildcarded path if you want a single pooled verdict instead.
"""
@property
def check_type(self) -> str:
return "at_least_one"
def validate_config(self, check_value: Any) -> None: # noqa: ANN401
"""Accept only a single-key mapping such as ``{primaryKey: true}``.
:raises ValueError: If ``check_value`` is not a mapping or does not
have exactly one key.
"""
if not isinstance(check_value, dict) or len(check_value) != 1:
raise ValueError(
f"'at_least_one' expects a single-key mapping such as {{primaryKey: true}}, got {check_value!r}"
)
def execute(
self,
attribute_path: str,
values: List[Any],
check_value: Any, # noqa: ANN401
) -> List[ValidationResult]:
"""Execute the at-least-one validation.
:param attribute_path: The dot-notation path to the collection being checked.
:param values: List of values found at the attribute path. Typically a list
containing one dict (a mapping of named items, e.g. fields in a model).
:param check_value: A mapping with exactly one ``attr: val`` entry — at
least one child object in the collection must have ``attr == val``.
Multi-key mappings, empty mappings, and non-mapping values are
rejected with a failing result.
:return: A list of :class:`ValidationResult` objects: a failing result
for null items inside the collection, plus a single verdict. An
absent attribute (empty ``values``) produces no results. Null
collections are reported by the validator.
"""
if not values:
return []
if not isinstance(check_value, dict) or len(check_value) != 1:
return [
ValidationResult(
"",
attribute_path,
self.check_type,
False,
f"Attribute '{attribute_path}' at_least_one check requires a "
f"single-key mapping (e.g. {{unique: true}}); got: {check_value!r}",
)
]
required_attr, required_val = next(iter(check_value.items()))
results = []
found = False
for value in values:
items: List[Any]
# A collection is either a dict keyed by item name (DCS style)
# or a list of named objects (ODCS style).
if isinstance(value, dict):
null_keys = [str(key) for key, item in value.items() if item is None]
if null_keys:
results.append(self.null_elements_result(attribute_path, len(null_keys), null_keys))
items = list(value.values())
elif isinstance(value, list):
null_count = sum(1 for item in value if item is None)
if null_count:
results.append(self.null_elements_result(attribute_path, null_count))
items = value
else:
items = []
for item in items:
if isinstance(item, dict) and item.get(required_attr) == required_val:
found = True
if found:
message = f"Attribute '{attribute_path}' contains at least one item with '{required_attr}: {required_val}'"
else:
message = f"Attribute '{attribute_path}' has no items with '{required_attr}: {required_val}'"
results.append(ValidationResult("", attribute_path, self.check_type, found, message))
return results
|
ExactlyOneCheck
Check that exactly one item in a collection satisfies a key-value condition.
Use this check when a constraint must hold for exactly one item in a
collection — not zero, not two, not more. The motivating case is primary
key enforcement: a model must have exactly one field with
primaryKey: true; two is a modelling bug.
Point the attribute path at the collection, not a leaf field, and
provide a single attr: val pair under exactly_one as a YAML block
mapping to match a nested attribute.
Example — every model must have exactly one primary key field:
data_contract_checks:
- attribute: models.*.fields
checks:
- exactly_one:
primaryKey: true
Example — every model must have exactly one event-time timestamp field:
data_contract_checks:
- attribute: models.*.fields
checks:
- exactly_one:
type: timestamp
Per-expansion semantics. When the attribute path contains a wildcard
(e.g. models.*.fields), the check runs once per expanded concrete path
— once per model in the examples above. Each expansion must independently
satisfy the condition; results are not aggregated across matches. Use a
non-wildcarded path if you want a single pooled verdict instead.
Check-value shape contract. check_value must be a single-key
mapping. Multi-key mappings, empty mappings, and non-mapping values are
rejected with a failing result.
check_value |
Result |
{primaryKey: true} |
OK — checked |
{primaryKey: true, unique: true} |
FAIL — multi-key rejected |
{} |
FAIL — empty mapping rejected |
true / "unique" / [...] / null |
FAIL — non-mapping rejected |
Source code in src/contract_checker/checks/validators/exactly_one_check.py
| class ExactlyOneCheck(Check):
"""Check that exactly one item in a collection satisfies a key-value condition.
Use this check when a constraint must hold for **exactly one** item in a
collection — not zero, not two, not more. The motivating case is primary
key enforcement: a model must have exactly one field with
``primaryKey: true``; two is a modelling bug.
Point the ``attribute`` path at the **collection**, not a leaf field, and
provide a single ``attr: val`` pair under ``exactly_one`` as a YAML block
mapping to match a nested attribute.
Example — every model must have exactly one primary key field:
```yaml
data_contract_checks:
- attribute: models.*.fields
checks:
- exactly_one:
primaryKey: true
```
Example — every model must have exactly one event-time timestamp field:
```yaml
data_contract_checks:
- attribute: models.*.fields
checks:
- exactly_one:
type: timestamp
```
**Per-expansion semantics.** When the attribute path contains a wildcard
(e.g. ``models.*.fields``), the check runs once per expanded concrete path
— once per model in the examples above. Each expansion must independently
satisfy the condition; results are *not* aggregated across matches. Use a
non-wildcarded path if you want a single pooled verdict instead.
**Check-value shape contract.** ``check_value`` must be a single-key
mapping. Multi-key mappings, empty mappings, and non-mapping values are
rejected with a failing result.
| ``check_value`` | Result |
|-------------------------------------------|--------------------------------|
| ``{primaryKey: true}`` | OK — checked |
| ``{primaryKey: true, unique: true}`` | FAIL — multi-key rejected |
| ``{}`` | FAIL — empty mapping rejected |
| ``true`` / ``"unique"`` / ``[...]`` / ``null`` | FAIL — non-mapping rejected |
"""
@property
def check_type(self) -> str:
return "exactly_one"
def validate_config(self, check_value: Any) -> None: # noqa: ANN401
"""Accept only a single-key mapping such as ``{primaryKey: true}``.
:raises ValueError: If ``check_value`` is not a mapping or does not
have exactly one key.
"""
if not isinstance(check_value, dict) or len(check_value) != 1:
raise ValueError(
f"'exactly_one' expects a single-key mapping such as {{primaryKey: true}}, got {check_value!r}"
)
def execute(
self,
attribute_path: str,
values: List[Any],
check_value: Any, # noqa: ANN401
) -> List[ValidationResult]:
"""Execute the exactly-one validation.
:param attribute_path: The dot-notation path to the collection being checked.
:param values: List of values found at the attribute path. Typically a list
containing one dict (a mapping of named items, e.g. fields in a model).
:param check_value: A single-key mapping ``{attr: val}`` — exactly one
child object in the collection must have ``attr == val``. Any other
shape (multi-key mapping, empty mapping, non-mapping) is rejected.
:return: A list of :class:`ValidationResult` objects: a failing result
for null items inside the collection, plus a single verdict. An
absent attribute (empty ``values``) produces no results. Null
collections are reported by the validator.
"""
if not values:
return []
if not isinstance(check_value, dict):
return [
ValidationResult(
"",
attribute_path,
self.check_type,
False,
f"Attribute '{attribute_path}' has invalid exactly_one check_value: "
f"expected a single-key mapping, got {type(check_value).__name__}",
)
]
if len(check_value) != 1:
return [
ValidationResult(
"",
attribute_path,
self.check_type,
False,
f"Attribute '{attribute_path}' has invalid exactly_one check_value: "
f"expected a single-key mapping, got {len(check_value)} keys",
)
]
required_attr, required_val = next(iter(check_value.items()))
results = []
count = 0
for value in values:
items: List[Any]
# A collection is either a dict keyed by item name (DCS style)
# or a list of named objects (ODCS style).
if isinstance(value, dict):
null_keys = [str(key) for key, item in value.items() if item is None]
if null_keys:
results.append(self.null_elements_result(attribute_path, len(null_keys), null_keys))
items = list(value.values())
elif isinstance(value, list):
null_count = sum(1 for item in value if item is None)
if null_count:
results.append(self.null_elements_result(attribute_path, null_count))
items = value
else:
items = []
for item in items:
if isinstance(item, dict) and item.get(required_attr) == required_val:
count += 1
passed = count == 1
if passed:
message = f"Attribute '{attribute_path}' contains exactly one item with '{required_attr}: {required_val}'"
else:
message = (
f"Attribute '{attribute_path}' has {count} items with "
f"'{required_attr}: {required_val}', expected exactly 1"
)
results.append(ValidationResult("", attribute_path, self.check_type, passed, message))
return results
|
MaxItemsCheck
Check if a list attribute does not exceed a maximum number of items.
The identifier for this check type ("max_items").
Example configuration in data-contract-checks.yml:
.. code-block:: yaml
data_contract_checks:
# Limit tags to maximum 5 items
- attribute: tags
checks:
- max_items: 5
# Limit examples to reasonable number
- attribute: models.*.fields.*.examples
checks:
- max_items: 10
Source code in src/contract_checker/checks/validators/items_check.py
| class MaxItemsCheck(Check):
"""Check if a list attribute does not exceed a maximum number of items.
The identifier for this check type (``"max_items"``).
Example configuration in ``data-contract-checks.yml``:
.. code-block:: yaml
data_contract_checks:
# Limit tags to maximum 5 items
- attribute: tags
checks:
- max_items: 5
# Limit examples to reasonable number
- attribute: models.*.fields.*.examples
checks:
- max_items: 10
"""
@property
def check_type(self) -> str:
"""Return the check type identifier.
:return: The string ``"max_items"``.
"""
return "max_items"
def validate_config(self, check_value: Any) -> None: # noqa: ANN401
"""Accept only a non-negative integer. See :func:`_validate_count_config`."""
_validate_count_config(self.check_type, check_value)
def execute(self, attribute_path: str, values: List[Any], max_count: int) -> List[ValidationResult]:
"""Execute the maximum items validation.
:param attribute_path: The dot-notation path to the attribute being checked.
:param values: List of values found at the attribute path. Each value should
be a list, or a mapping of named items (e.g. DCS ``fields``).
:param max_count: The maximum number of items allowed in the list.
:return: A list of :class:`ValidationResult` objects: one failing result
per list or mapping with null elements, plus a single count result.
Null elements are not counted. A value that is not a list or
mapping fails. An absent attribute (empty ``values``)
produces no results. Null values are reported by the validator.
"""
if not values:
return []
if not all(isinstance(value, (list, dict)) for value in values):
return [_not_a_collection_result(attribute_path, self.check_type)]
results = []
all_passed = True
for value in values:
count, null_results = _count_items(self, attribute_path, value)
results.extend(null_results)
if count > max_count:
all_passed = False
if all_passed:
message = f"Attribute '{attribute_path}' has at most {max_count} items"
else:
message = f"Attribute '{attribute_path}' has more than {max_count} items"
results.append(ValidationResult("", attribute_path, self.check_type, all_passed, message))
return results
|
MinItemsCheck
Check if a list attribute has at least a minimum number of items.
The identifier for this check type ("min_items").
Example configuration in data-contract-checks.yml:
data_contract_checks:
# Ensure each model has at least one field
- attribute: models.*.fields
checks:
- min_items: 1
# Ensure at least 3 examples are provided
- attribute: models.*.fields.*.examples
checks:
- min_items: 3
Source code in src/contract_checker/checks/validators/items_check.py
| class MinItemsCheck(Check):
"""Check if a list attribute has at least a minimum number of items.
The identifier for this check type (``"min_items"``).
Example configuration in ``data-contract-checks.yml``:
``` yaml
data_contract_checks:
# Ensure each model has at least one field
- attribute: models.*.fields
checks:
- min_items: 1
# Ensure at least 3 examples are provided
- attribute: models.*.fields.*.examples
checks:
- min_items: 3
```
"""
@property
def check_type(self) -> str:
"""Return the check type identifier.
:return: The string ``"min_items"``.
"""
return "min_items"
def validate_config(self, check_value: Any) -> None: # noqa: ANN401
"""Accept only a non-negative integer. See :func:`_validate_count_config`."""
_validate_count_config(self.check_type, check_value)
def execute(self, attribute_path: str, values: List[Any], min_count: int) -> List[ValidationResult]:
"""Execute the minimum items validation.
:param attribute_path: The dot-notation path to the attribute being checked.
:param values: List of values found at the attribute path. Each value should
be a list, or a mapping of named items (e.g. DCS ``fields``).
:param min_count: The minimum number of items required in the list.
:return: A list of :class:`ValidationResult` objects: one failing result
per list or mapping with null elements, plus a single count result.
Null elements are not counted. A value that is not a list or
mapping fails. An absent attribute (empty ``values``)
produces no results. Null values are reported by the validator.
"""
if not values:
return []
if not all(isinstance(value, (list, dict)) for value in values):
return [_not_a_collection_result(attribute_path, self.check_type)]
results = []
all_passed = True
for value in values:
count, null_results = _count_items(self, attribute_path, value)
results.extend(null_results)
if count < min_count:
all_passed = False
if all_passed:
message = f"Attribute '{attribute_path}' has at least {min_count} items"
else:
message = f"Attribute '{attribute_path}' has fewer than {min_count} items"
results.append(ValidationResult("", attribute_path, self.check_type, all_passed, message))
return results
|
PatternCheck
Check if attribute values match a regular expression pattern.
The identifier for this check type is ("pattern").
Example configuration in data-contract-checks.yml:
data_contract_checks:
# Ensure title starts with "Contract"
- attribute: info.title
checks:
- pattern: ^Contract
# Ensure version follows semantic versioning
- attribute: info.version
checks:
- pattern: ^\d+\.\d+\.\d+$
# Ensure field names are snake_case
- attribute: models.*.fields.*.name
checks:
- pattern: ^[a-z][a-z0-9_]*$
# Ensure every tag is lowercase (list attribute: each item is checked)
- attribute: tags
checks:
- pattern: ^[a-z]+$
To enforce full-string matching implicitly, switch this to re.fullmatch.
Source code in src/contract_checker/checks/validators/pattern_check.py
| class PatternCheck(Check):
"""Check if attribute values match a regular expression pattern.
The identifier for this check type is (``"pattern"``).
Example configuration in ``data-contract-checks.yml``:
``` yaml
data_contract_checks:
# Ensure title starts with "Contract"
- attribute: info.title
checks:
- pattern: ^Contract
# Ensure version follows semantic versioning
- attribute: info.version
checks:
- pattern: ^\\d+\\.\\d+\\.\\d+$
# Ensure field names are snake_case
- attribute: models.*.fields.*.name
checks:
- pattern: ^[a-z][a-z0-9_]*$
# Ensure every tag is lowercase (list attribute: each item is checked)
- attribute: tags
checks:
- pattern: ^[a-z]+$
```
To enforce full-string matching implicitly, switch this to ``re.fullmatch``.
"""
@property
def check_type(self) -> str:
"""Return the check type identifier.
:return: The string ``"pattern"``.
"""
return "pattern"
def validate_config(self, check_value: Any) -> None: # noqa: ANN401
"""Accept only a string that compiles as a regular expression.
:raises ValueError: If ``check_value`` is not a string or is not a valid regex.
"""
if not isinstance(check_value, str):
raise ValueError(f"'pattern' expects a regular expression string, got {type(check_value).__name__}")
try:
re.compile(check_value)
except re.error as e:
raise ValueError(f"'pattern' is not a valid regular expression ({check_value!r}): {e}") from e
def execute(self, attribute_path: str, values: List[Any], pattern: str) -> List[ValidationResult]:
"""Execute the pattern matching validation.
:param attribute_path: The dot-notation path to the attribute being checked.
:param values: List of values found at the attribute path. Each value can
be a scalar or a list of scalars; items are converted to a string
before pattern matching.
:param pattern: The regular expression pattern to match against.
:return: A list of :class:`ValidationResult` objects, one for each scalar
value. When the attribute contains a list, a result is generated for
each item in that list. An absent attribute (empty ``values``)
produces no results, since presence is enforced by the
``required`` check. Top-level null values are reported by the
validator; null items inside a list produce one failed result.
"""
results = []
items: List[Any] = []
for value in values:
if isinstance(value, list):
null_count = sum(1 for item in value if item is None)
if null_count:
results.append(self.null_elements_result(attribute_path, null_count))
items.extend(item for item in value if item is not None)
else:
items.append(value)
for value in items:
value_str = str(value)
passed = re.match(pattern, value_str) is not None
if passed:
message = f"Attribute '{attribute_path}' value '{value_str}' matches pattern '{pattern}'"
else:
message = f"Attribute '{attribute_path}' value '{value_str}' does not match pattern '{pattern}'"
results.append(ValidationResult("", attribute_path, self.check_type, passed, message))
return results
|
RequiredCheck
Check if a required field exists and is not null.
The identifier for this check type is ("required").
Example configuration in data-contract-checks.yml:
data_contract_checks:
- attribute: info.title
checks:
- required: true
- attribute: models.*.fields.*.description
checks:
- required: true
Source code in src/contract_checker/checks/validators/required_check.py
| class RequiredCheck(Check):
"""Check if a required field exists and is not null.
The identifier for this check type is (``"required"``).
Example configuration in ``data-contract-checks.yml``:
``` yaml
data_contract_checks:
- attribute: info.title
checks:
- required: true
- attribute: models.*.fields.*.description
checks:
- required: true
```
"""
@property
def check_type(self) -> str:
"""Return the check type identifier.
:return: The string ``"required"``.
"""
return "required"
# required reports null values itself ("Required attribute ... is null"),
# and required: false must stay a no-op even when the value is null.
handles_nulls = True
def validate_config(self, check_value: Any) -> None: # noqa: ANN401
"""Accept only ``true`` or ``false``.
A bare ``required:`` (YAML null) is rejected rather than treated as
``false``: it is almost always a forgotten ``true``, and ``required:
false`` exists for the intentional case.
:raises ValueError: If ``check_value`` is anything else.
"""
if check_value is None:
raise ValueError("'required' has no value (did you mean 'required: true'?)")
if not isinstance(check_value, bool):
raise ValueError(f"'required' expects true or false, got {check_value!r}")
def execute(self, attribute_path: str, values: List[Any], check_value: bool) -> List[ValidationResult]:
"""Execute the required field validation.
:param attribute_path: The dot-notation path to the attribute being checked
(e.g., ``"info.title"`` or ``"models.*.fields.*.name"``).
:param values: List of values found at the attribute path.
:param check_value: ``True``: the attribute must exist and be non-null.
``False``: the check does nothing and returns no results. Anything
else is rejected (see :meth:`validate_config`).
:return: A list containing a single :class:`ValidationResult` indicating
whether the check passed or failed, or an empty list when
``check_value`` is ``False``.
:raises ValueError: If ``check_value`` is not a boolean.
"""
self.validate_config(check_value)
if check_value is False:
# required: false documents that a field is optional; it checks
# nothing, so it must not add a (misleading) passing result.
return []
if not values:
passed = False
message = f"Required attribute '{attribute_path}' is missing"
elif any(v is None for v in values):
passed = False
message = f"Required attribute '{attribute_path}' is null"
else:
passed = True
message = f"Attribute '{attribute_path}' is present"
return [ValidationResult("", attribute_path, self.check_type, passed, message)]
|
This page contains information about the validation checks available in data-contract-checker.
at_least_one
Passes when at least one item in a collection satisfies a key-value condition. Use this when a constraint must hold for some items rather than all items — for example, enforcing that every model has at least one unique field.
Point the attribute path at the collection (not the leaf attribute), and specify a single key-value pair as the check value using a YAML block mapping:
data_contract_checks:
# Every model must have at least one field with unique: true
- attribute: models.*.fields
checks:
- at_least_one:
unique: true
# Every model must have at least one primary key field
- attribute: models.*.fields
checks:
- at_least_one:
primaryKey: true
Why point at the collection? Wildcard paths like models.*.fields.*.unique are expanded and checked per field. Stopping one level up at models.*.fields passes the entire fields dict to the check, so it can inspect all fields together.
Per-model, not aggregated. When the attribute path contains a wildcard (e.g. models.*.fields), the check runs once per expanded path — once for each model in this example. Each model must independently satisfy the condition; results are not pooled across models. If two of three models have a unique: true field and one does not, you'll get two pass results and one fail, not a single combined verdict. To require that at least one unique field exists somewhere across all models, point attribute at a non-wildcarded path instead.
Check value shape. at_least_one requires a single-key mapping, for example:
checks:
- at_least_one:
unique: true
Multi-key mappings, empty mappings, and non-mapping values (scalars, lists) are rejected with a failing result.
exactly_one
Passes when exactly one item in a collection satisfies a key-value condition — not zero, not two, not more. Use this when a constraint must hold for a single designated item. The motivating case is primary-key enforcement: a model with two primaryKey: true fields is a modelling bug that at_least_one silently accepts.
Point the attribute path at the collection (not the leaf attribute), and specify a single key-value pair as the check value using a YAML block mapping:
data_contract_checks:
# Every model must have exactly one primary key field
- attribute: models.*.fields
checks:
- exactly_one:
primaryKey: true
# Every model must have exactly one event-time timestamp field
- attribute: models.*.fields
checks:
- exactly_one:
type: timestamp
Failure messages include the actual count, e.g. has 2 items with 'primaryKey: true', expected exactly 1.
Why point at the collection? Wildcard paths like models.*.fields.*.primaryKey are expanded and checked per field. Stopping one level up at models.*.fields passes the entire fields dict to the check, so it can count all matching fields together.
Per-model, not aggregated. When the attribute path contains a wildcard (e.g. models.*.fields), the check runs once per expanded path — once for each model in this example. Each model must independently have exactly one match; results are not pooled across models. To require a single designated item across all models, point attribute at a non-wildcarded path instead.
Check-value shape contract. exactly_one requires a single-key mapping. Anything else fails loudly:
check_value |
Result |
{primaryKey: true} |
OK — checked |
{primaryKey: true, unique: true} |
FAIL — multi-key rejected |
{} |
FAIL — empty mapping rejected |
true / "unique" / [primaryKey] / null |
FAIL — non-mapping rejected |