General
Lutaml::Model provides comprehensive validation for data models using the validate and validate! methods.
-
The
validatemethod returns anerrorsarray containing all validation errors. This method is used for checking the validity of the model silently. -
The
validate!method raises aLutaml::Model::ValidationErrorthat contains all the validation errors. This method is used for forceful validation of the model through raising an error.
Validation types
Lutaml::Model supports the following validation types:
Collection validation
The collection option sets an attribute’s cardinality: collection: true means 0..*, a range such as (1..) or (0..5) bounds the count, and omitting it (the default) means 0..1, a single value.
attribute :items, :string, collection: (1..) # At least one item
attribute :tags, :string, collection: (0..5) # Up to 5 tags
attribute :name, :string # Default 0..1 (single value)A non-collection attribute given multiple values keeps them. Parsing does not raise; the values land in the attribute as an array and #validate reports Lutaml::Model::CollectionTrueMissingError, which #validate! raises wrapped in a Lutaml::Model::ValidationError.
obj = Person.from_xml("<person><name>A</name><name>B</name></person>")
obj.name # => ["A", "B"]
obj.validate # => [#<Lutaml::Model::CollectionTrueMissingError ...>]
obj.validate! # raises Lutaml::Model::ValidationErrorDeclare collection: true for attributes that hold many values.
The rule is about shape, not count. A singular attribute is declared to hold a value, not a list, so a one-element array is still a violation:
Person.from_json('{"name": ["A"]}').validate
# => [#<Lutaml::Model::CollectionTrueMissingError ...>]XML has no such distinction — a single <name> element is a value, so only a repeated element trips the check.
An invalid document does not round-trip. While an attribute holds more values than it was declared for, serializing it back out will not reproduce the input. Fix the violation and it round-trips normally.
Two cases still report at parse time rather than on #validate:
-
A declared range that is exceeded raises
Lutaml::Model::CollectionCountOutOfRangeErrorfor key/value formats. -
A model mapped onto a plain Ruby class with
modelhas no#validate, so there is no later point to report to.
A custom deserializer (with: { from: }) receives whatever arrived — a single node for one occurrence, an array for several, the same shape a custom to: method already receives for a collection. If that method collapses the array to a single value, the over-count is not reported: the method has decided what the attribute holds.
Value enumeration validation
The values option validates that an attribute value is one of a fixed set of values.
attribute :status, :string, values: %w[draft published archived]Choice validation
The choice directive validates that attributes within a defined range are present.
choice(min: 1, max: 2) do
attribute :email, :string
attribute :phone, :string
endPattern validation
The pattern option validates string values against regular expressions.
attribute :email, :string, pattern: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/iRequired attributes
The required: true option validates that an attribute is present.
attribute :name, :string, required: trueXML schema validation
The validate_xml_with class macro validates the model’s generated XML against one or more XML Schema (XSD) files during validate / validate!.
class Person < Lutaml::Model::Serializable
attribute :name, :string
xml do
root "person"
map_element "name", to: :name
end
validate_xml_with "schemas/person.xsd"
end
person = Person.new(name: "Alice")
person.validate # => collects Lutaml::Xml::Error::SchemaValidationError
# instances when the generated XML does not conform
person.validate! # => raises Lutaml::Model::ValidationError on nonconformanceThe macro accepts one or more schema paths, and repeated calls append to the configured list. Subclasses inherit the parent’s schema paths parent-first; paths added by a subclass do not affect the parent.
validate_xml_with "schemas/base.xsd", "schemas/extensions.xsd"Raw XML strings can be checked against the same schemas with the explicit class-level helpers, for example to reject nonconforming input before from_xml:
Person.validate_xml(xml_string) # => array of schema validation errors
Person.validate_xml!(xml_string) # => raises Lutaml::Model::ValidationError to_xml and from_xml never validate implicitly. Call validate! before serializing, or validate_xml! on incoming XML, when enforcement is needed — the same explicit contract as every other validation type. |
XSD validation uses Nokogiri, which is required lazily on first validation. The schemas must be local files declared explicitly; xsi:schemaLocation in documents is not read and remote schemas are not fetched. Relative paths resolve against the file that declares validate_xml_with, so a model works regardless of the process working directory.
Misconfiguration raises instead of being collected as validation errors:
-
a missing schema file raises
Errno::ENOENT; -
a malformed XSD raises
Nokogiri::XML::SyntaxError; -
a malformed XML string passed to
validate_xml/validate_xml!raisesNokogiri::XML::SyntaxError— parsing is strict, so malformed input is never silently repaired and reported as conforming; -
a missing nokogiri gem raises
Lutaml::Xml::Error::XmlConfigurationError; -
a class using the macro without an XML root mapping raises
Lutaml::Model::TypeOnlyMappingErrorfromvalidate, since validation must serialize the model to check it.
Validation examples
The following class will validate the name is present and degree_settings attributes to ensure that it has at least one element and that the description attribute is one of the values in the set [one, two, three].
class Klin < Lutaml::Model::Serializable
attribute :name, :string, required: true
attribute :degree_settings, :integer, collection: (1..)
attribute :description, :string, values: %w[one two three]
attribute :id, :integer
attribute :age, :integer
choice(min: 1, max: 1) do
choice(min: 1, max: 2) do
attribute :prefix, :string
attribute :forename, :string
end
attribute :nick_name, :string
end
xml do
map_element 'name', to: :name
map_attribute 'degree_settings', to: :degree_settings
end
end
klin = Klin.new(name: "Klin", degree_settings: [100, 200, 300], description: "one", prefix: "Ben")
klin.validate
# => []
klin = Klin.new(name: "Klin", degree_settings: [], description: "four", prefix: "Ben", nick_name: "Smith")
klin.validate
# => [
# #<Lutaml::Model::CollectionSizeError: degree_settings must have at least 1 element>,
# #<Lutaml::Model::ValueError: description must be one of [one, two, three]>,
# #<Lutaml::Model::ChoiceUpperBoundError: Attribute count exceeds the upper bound>
# ]
e = klin.validate!
# => Lutaml::Model::ValidationError: [
# degree_settings must have at least 1 element,
# description must be one of [one, two, three],
# Attribute count exceeds the upper bound
# ]
e.errors
# => [
# #<Lutaml::Model::CollectionSizeError: degree_settings must have at least 1 element>,
# #<Lutaml::Model::ValueError: description must be one of [one, two, three]>,
# #<Lutaml::Model::ChoiceUpperBoundError: Attribute count exceeds the upper bound>
# #<Lutaml::Model::ChoiceLowerBoundError: Attribute count is less than lower bound>
# ]
klin = Klin.new(degree_settings: [100, 200, 300], description: "one", prefix: "Ben")
klin.validate
# => [
# #<Lutaml::Model::RequiredAttributeMissingError: Missing required attribute: name>
# ]Custom validation
To add custom validation, override the validate method in the model class. Additional errors should be added to the errors array.
The following class validates the degree_settings attribute when the type is glass to ensure that the value is less than 1300.
class Klin < Lutaml::Model::Serializable
attribute :name, :string
attribute :type, :string, values: %w[glass ceramic]
attribute :degree_settings, :integer, collection: (1..)
def validate
errors = super
if type == "glass" && degree_settings.any? { |d| d > 1300 }
errors << Lutaml::Model::Error.new("Degree settings for glass must be less than 1300")
end
end
end
klin = Klin.new(name: "Klin", type: "glass", degree_settings: [100, 200, 1400])
klin.validate
# => [#<Lutaml::Model::Error: Degree settings for glass must be less than 1300>]