Fork me on GitHub

Validating with Constraints

This guide explains how to define and validate Metaschema constraints.

Metaschema constraints provide validation beyond schema structure:

  • Cardinality - Required fields, occurrence limits
  • Allowed values - Enumerated restrictions
  • Patterns - Regex-based validation
  • Uniqueness - Key constraints
  • Cross-references - Reference integrity
  • Custom rules - Metapath-based assertions

Restrict values to a defined set:

<define-flag name="status">
    <constraint>
        <allowed-values allow-other="no">
            <enum value="active">Active status</enum>
            <enum value="inactive">Inactive status</enum>
            <enum value="pending">Pending status</enum>
        </allowed-values>
    </constraint>
</define-flag>

Validate against a regular expression:

<define-flag name="code">
    <constraint>
        <matches pattern="[A-Z]{2}-[0-9]{4}" />
    </constraint>
</define-flag>

Control occurrence requirements:

<define-assembly name="catalog">
    <model>
        <field ref="title">
            <constraint>
                <has-cardinality min-occurs="1" max-occurs="1" />
            </constraint>
        </field>
    </model>
</define-assembly>

Ensure unique values within a scope:

<define-assembly name="catalog">
    <constraint>
        <index name="control-id-index" target="//control">
            <key-field target="@id" />
        </index>
    </constraint>
</define-assembly>

Validate references point to existing values:

<define-assembly name="profile">
    <constraint>
        <index-has-key name="control-reference-check"
                       index-name="control-id-index"
                       target="//include-controls/with-id">
            <key-field target="." />
        </index-has-key>
    </constraint>
</define-assembly>

Custom Metapath-based validation:

<define-assembly name="metadata">
    <constraint>
        <expect test="last-modified >= published"
                message="Last modified must be after published date" />
    </constraint>
</define-assembly>
import dev.metaschema.databind.IBindingContext;
import dev.metaschema.databind.io.DeserializationFeature;
import dev.metaschema.databind.io.IBoundLoader;

import java.nio.file.Path;

IBindingContext context = IBindingContext.newInstance();
IBoundLoader loader = context.newBoundLoader();

// Enable constraint validation during loading
loader.enableFeature(DeserializationFeature.DESERIALIZE_VALIDATE_CONSTRAINTS);

Object model = loader.load(Path.of("data.json"));
import dev.metaschema.core.model.validation.IValidationResult;
import dev.metaschema.databind.IBindingContext;

import java.net.URI;
import java.nio.file.Path;

IBindingContext context = IBindingContext.newInstance();
URI target = Path.of("data.json").toUri();

IValidationResult result = context.validateWithConstraints(target, null);

if (!result.isPassing()) {
    result.getFindings().forEach(finding -> {
        System.err.println(finding.getSeverity() + ": " +
            finding.getMessage() + " at " + finding.getLocation());
    });
}
import dev.metaschema.core.model.constraint.IConstraint.Level;
import dev.metaschema.core.model.validation.IValidationFinding;
import dev.metaschema.core.model.validation.IValidationResult;
import dev.metaschema.databind.IBindingContext;

import java.net.URI;
import java.nio.file.Path;

IBindingContext context = IBindingContext.newInstance();
URI target = Path.of("data.json").toUri();

IValidationResult result = context.validateWithConstraints(target, null);

// Check overall status
if (result.isPassing()) {
    System.out.println("Validation passed");
}

// Process findings by severity
for (IValidationFinding finding : result.getFindings()) {
    Level severity = finding.getSeverity();
    if (severity == Level.CRITICAL) {
        handleCritical(finding);
    } else if (severity == Level.ERROR) {
        handleError(finding);
    } else if (severity == Level.WARNING) {
        handleWarning(finding);
    } else {
        logInfo(finding);
    }
}

The framework provides FindingCollectingConstraintValidationHandler for collecting validation findings:

import dev.metaschema.core.model.constraint.FindingCollectingConstraintValidationHandler;
import dev.metaschema.core.model.constraint.IConstraint.Level;
import dev.metaschema.core.model.validation.IValidationResult;

// The handler implements IValidationResult
FindingCollectingConstraintValidationHandler handler =
    new FindingCollectingConstraintValidationHandler();

// After validation completes, check results
if (!handler.isPassing()) {
    handler.getFindings().forEach(finding -> {
        System.err.println(finding.getSeverity() + ": " +
            finding.getMessage());
    });
}

// Check highest severity level
Level highestSeverity = handler.getHighestSeverity();
if (highestSeverity.ordinal() >= Level.ERROR.ordinal()) {
    System.err.println("Validation failed with errors");
}

Control validation behavior with levels:

<constraint>
    <!-- Causes validation failure -->
    <expect level="ERROR" test="@id" message="ID is required" />

    <!-- Logged but doesn't fail -->
    <expect level="WARNING" test="title" message="Title recommended" />

    <!-- Informational only -->
    <expect level="INFORMATIONAL" test="version" message="Consider adding version" />
</constraint>

Define constraints in separate files:

<!-- main-module.xml -->
<metaschema>
    <import-constraints href="additional-constraints.xml" />
</metaschema>

<!-- additional-constraints.xml -->
<constraints>
    <context>
        <metapath>/catalog//control</metapath>
        <constraints>
            <expect test="title" message="Controls must have titles" />
        </constraints>
    </context>
</constraints>
<constraint>
    <expect test="title" message="Title is required" />
</constraint>
<constraint>
    <expect test="not(@type = 'formal') or description"
            message="Formal items require descriptions" />
</constraint>
<constraint>
    <expect test="@count >= 0 and @count <= 100"
            message="Count must be between 0 and 100" />
</constraint>
<constraint>
    <expect test="end-date >= start-date"
            message="End date must be after start date" />
</constraint>
<constraint>
    <is-unique target="item" name="unique-item-id">
        <key-field target="@id" />
    </is-unique>
</constraint>
Level Meaning Effect
CRITICAL Severe error Document unusable
ERROR Constraint violation Validation fails
WARNING Potential issue Logged, doesn't fail
INFORMATIONAL Note Logged only
  1. Use appropriate levels - Not every issue is an ERROR
  2. Provide clear messages - Include context and fix hints
  3. Validate early - Check on load, not later
  4. Handle all severities - Don't ignore warnings
  5. Test constraints - Validate with known-bad data

Note: Metaschema constraint validation is experimental in some areas. Schema-level validation (structure, types) is stable. Advanced constraint types may have limitations.

Continue learning about the Metaschema Java Tools with these related guides: