General

Basic attributes

An attribute is the basic building block of a model. It is a named value that stores a single piece of data (which may be one or multiple pieces of data).

An attribute only accepts the type of value defined in the attribute definition.

The attribute value type can be one of the following:

  • Value (inherits from Lutaml::Model::Value)

  • Model (inherits from Lutaml::Model::Serializable)

Syntax:

attribute :name_of_attribute, Type

Where,

name_of_attribute

The defined name of the attribute.

Type

The type of the attribute.

Example 1. Using the attribute class method to define simple attributes
class Studio < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :address, :string
  attribute :established, :date
end
s = Studio.new(name: 'Pottery Studio', address: '123 Clay St', established: Date.new(2020, 1, 1))
puts s.name
#=> "Pottery Studio"
puts s.address
#=> "123 Clay St"
puts s.established
#=> <Date: 2020-01-01>

Restricting the value of an attribute

The restrict class method is used to update or refine the validation rules for an attribute that has already been defined. This allows you to apply additional or stricter constraints to an existing attribute without redefining it.

Example 2. Using the restrict class method to update the options of an existing attribute
class Studio < Lutaml::Model::Serializable
  attribute :name, :string
  restrict :name, collection: 1..3, pattern: /[A-Za-z]+/
end
Example 3. Apply different restrictions to the existing attribute in multiple subclasses
class Document < Lutaml::Model::Serializable
  attribute :status, :string
end

class DraftDocument < Document
  # Only allow "draft" or "in_review" as valid statuses for drafts
  restrict :status, values: %w[draft in_review]
end

class PublishedDocument < Document
  # Only allow "published" or "archived" as valid statuses for published documents
  restrict :status, values: %w[published archived]
end

# Usage
# Call .validate! to trigger validation and raise an error if the value is not allowed
Document.new(status: "draft").validate!                # valid, there are no validation rules for `Document`
Document.new(status: "published").validate!            # valid, there are no validation rules for `Document`
DraftDocument.new(status: "draft").validate!           # valid
DraftDocument.new(status: "in_review").validate!       # valid
DraftDocument.new(status: "published").validate!       # raises error (not allowed)
PublishedDocument.new(status: "published").validate!   # valid
PublishedDocument.new(status: "archived").validate!    # valid
PublishedDocument.new(status: "draft").validate!       # raises error (not allowed)

All options that are supported by the attribute class method are also supported by the restrict method. Any unsupported option passed to restrict will result in a Lutaml::Model::InvalidAttributeOptionsError being raised.

Polymorphic attributes

General

A polymorphic attribute is an attribute that can accept multiple types of values. This is useful when the attribute defines common characteristics and behaviors among different types.

An attribute with a defined value type also accepts values that are of a class that is a subclass of the defined type.

The assigned attribute of Type accepts polymorphic classes as long as the assigned instance is of a class that either inherits from the declared type or matches it.

Naïve approach does not work…​

A naïve polymorphic approach is to define an attribute with a superclass type and assign instances of subclasses to it.

While this approach works (somewhat) in modeling, it does not work with serialization (half) or deserialization (not at all).

The following example illustrates why such approach is naïve.

Example 4. An attribute receiving the superclass type accepts subclass instances
class Studio < Lutaml::Model::Serializable
  attribute :name, :string
end

# CeramicStudio is a specialization of Studio
class CeramicStudio < Studio
  attribute :clay_type, :string
end

class PotteryClass < Lutaml::Model::Serializable
  # the :studio attribute should accept Studio and CeramicStudio
  attribute :studio, Studio
end
# This works
> s = Studio.new(name: 'Pottery Studio')
> p = PotteryClass.new(studio: s)
> p.studio
# => <Studio:0x0000000104ac7240 @name="Pottery Studio", @address=nil, @established=nil>

# A subclass of Studio is also valid
> s = CeramicStudio.new(name: 'Ceramic World', clay_type: 'Red')
> p = PotteryClass.new(studio: s)
> p.studio
# => <CeramicStudio:0x0000000104ac7240 @name="Ceramic World", @address=nil, @established=nil, @clay_type="Red">
> p.studio.name
# => "Ceramic World"
> p.studio.clay_type
# => "Red"

So far so good. However, this approach does not work in serialization. This is what happens when we call to_yaml on the PotteryClass instance.

> puts p.to_yaml
# => ---
# => studio:
# =>   name: Ceramic World
# =>   clay_type: Red

When deserializing the YAML string, the studio attribute will be deserialized as an instance of Studio, not CeramicStudio. This means that the clay_type attribute will be lost.

> p = PotteryClass.load_yaml("---\nstudio:\n  name: Ceramic World\n  clay_type: Red")
> p.studio
# => <Studio:0x0000000104ac7240 @name="Ceramic World">
> p.studio.clay_type
# => ERROR

Proper polymorphic approaches

Lutaml::Model offers rich support for polymorphic attributes, through configuration at both attribute and serialization levels.

In polymorphism, there are the following components:

polymorphic attribute

the attribute that can be assigned multiple types.

polymorphic attribute class

the class that has a polymorphic attribute.

polymorphic superclass

a class assigned to a polymorphic attribute that serves as the superclass for all accepted polymorphic classes.

polymorphic subclass

a class that is a subclass of the polymorphic superclass and can be assigned to the polymorphic attribute. There are often more than 2 subclasses in a scenario since polymorphism is meant to apply to multiple types.

To utilize polymorphic attributes, modification to all of these components are necessary.

In serialized form, polymorphic classes are differentiated by an explicit "polymorphic class differentiator".

Example 5. Sample serialization of polymorphic classes in YAML

In key-value formats like YAML, the polymorphic class differentiator is typically a key-value pair that contains the polymorphic class name.

references:
- _class: Document # This is a DocumentReference
  name: "The Tibetan Book of the Dead"
  document_id: "book:tbtd"
- _class: Anchor # This is an AnchorReference
  name: "Chapter 1"
  anchor_id: "book:tbtd:anchor-1"
Example 6. Sample serialization of polymorphic classes in XML

In XML, the polymorphic class differentiator is typically an attribute that contains the polymorphic class name.

<references>
  <!-- The "document-ref" value is a DocumentReference -->
  <reference reference-type="document-ref">
    <name>The Tibetan Book of the Dead</name>
    <document_id>book:tbtd</document_id>
  </reference>
  <!-- The "anchor-ref" value is an AnchorReference -->
  <reference reference-type="anchor-ref">
    <name>Chapter 1</name>
    <anchor_id>book:tbtd:anchor-1</anchor_id>
  </reference>
</references>
While it is possible to determine different polymorphic classes based on the attributes they contain, such mechanism would not be able to determine the polymorphic class if serializations of two polymorphic subclasses can be identical.

There are two basic scenarios in using polymorphic attributes:

Please refer to spec/lutaml/model/polymorphic_spec.rb for full examples of implementing polymorphic attributes.

Defining the polymorphic attribute

The polymorphic attribute class is a class that has a polymorphic attribute.

At this level, the polymorphic option is used to specify the types that the polymorphic attribute can accept.

class PolymorphicAttributeClass < Lutaml::Model::Serializable
  attribute :attribute_name, (1)
    {polymorphic-superclass-class}, (2)
    {options}, (3)
    polymorphic: [ (4)
      polymorphic-subclass-1, (5)
      polymorphic-subclass-2,
    ]
end
1 The name of the polymorphic attribute.
2 The polymorphic superclass class.
3 Any options for the attribute.
4 The polymorphic option that determines the acceptable polymorphic subclasses, or just true.
5 The polymorphic subclasses.

The polymorphic option is an array of polymorphic subclasses that the attribute can accept.

These options enable the following scenarios.

  • If the polymorphic attribute is to only contain instances of the polymorphic-superclass-class, not its subclasses, then the polymorphic option is not needed.

    In the following code, ReferenceSet has an attribute references that only accepts instances of Reference. The polymorphic option does not apply.

    class ReferenceSet < Lutaml::Model::Serializable
      attribute :references, Reference, collection: true
    end
  • If the attribute (collection or not) is meant to only contain one type of polymorphic subclasses, then the polymorphic option is also not needed, because the polymorphic subclass can be stated as the attribute value type.

    In the following code, ReferenceSet has an attribute references that only accepts instances of DocumentReference, a subclass of Reference. The polymorphic option does not apply.

    class ReferenceSet < Lutaml::Model::Serializable
      attribute :references, DocumentReference, collection: true
    end
  • If the attribute (collection or not) is meant to contain instances belonging to any polymorphic subclass of a defined base class, then set the polymorphic: true option.

    In the following code, ReferenceSet is a class that has a polymorphic attribute references. The references attribute can accept instances of any polymorphic subclass of the Reference base class, so polymorphic: true is set.

    class ReferenceSet < Lutaml::Model::Serializable
      attribute :references, Reference, collection: true, polymorphic: true
    end
  • If the attribute (collection or not) is meant to contain instances belonging to more than one polymorphic subclass, then those acceptable polymorphic subclasses should be explicitly specified in the polymorphic: […​] option.

    In the following code, ReferenceSet is a class that has a polymorphic attribute references. The references attribute can accept instances of DocumentReference and AnchorReference, both of which are subclasses of Reference.

    class ReferenceSet < Lutaml::Model::Serializable
      attribute :references, Reference, collection: true, polymorphic: [
        DocumentReference,
        AnchorReference,
      ]
    end

Differentiating polymorphic subclasses

General

A polymorphic subclass needs an additional attribute with the polymorphic_class option to allow Lutaml::Model for identifying itself in serialization. This attribute is called the "polymorphic class differentiator".

There are two methods for setting the polymorphic class differentiator:

  • Setting the polymorphic class differentiator in the polymorphic superclass, as polymorphic subclasses inherit from it (relying on [model-inheritance]).

  • Setting the polymorphic class differentiator in the individual polymorphic subclasses

Setting the differentiator in the polymorphic superclass

The polymorphic class differentiator can be set in the polymorphic superclass. This scenario fits best if there are many polymorphic subclasses and the polymorphic superclass can be modified.

Syntax:

Setting the polymorphic differentiator in the superclass
class PolymorphicSuperclass < Lutaml::Model::Serializable
  attribute :{_polymorphic_differentiator}, (1)
    :string, (2)
    polymorphic_class: true (3)
  # ...
end
1 The polymorphic differentiator is a normal attribute that can be assigned to any name.
2 The polymorphic differentiator must have a value type of :string.
3 The option for polymorphic_class must be set to true to indicate that this attribute accepts subclass types.
Setting the differentiator in the individual polymorphic subclasses

The polymorphic class differentiator can be set in the individual polymorphic subclasses. This scenario fits best if there are few polymorphic subclasses and the polymorphic superclass cannot be modified.

Syntax:

Setting the polymorphic differentiator in the subclass
# No modification to the superclass is needed.
class PolymorphicSuperclass < Lutaml::Model::Serializable
  # ...
end

# The polymorphic differentiator is set in the subclass.
class PolymorphicSubclass < PolymorphicSuperclass
  attribute
    :{_polymorphic_differentiator}, (1)
    :string, (2)
    polymorphic_class: true (3)
  # ...
end
1 The polymorphic differentiator is a normal attribute that can be assigned to any name.
2 The polymorphic differentiator must have a value type of :string.
3 The option for polymorphic_class must be set to true to indicate that this attribute accepts subclass types.

Polymorphic differentiation in serialization

General

The polymorphic attribute class needs to determine what class to use based on the serialized value of the polymorphic differentiator.

The polymorphic attribute class mapping is format-independent, allowing for differentiation of polymorphic subclasses in different serialization formats.

The mapping of the serialized polymorphic differentiator can be set in either:

  • the polymorphic superclass; or

  • the polymorphic attribute class and the individual polymorphic subclasses.

Mapping in the polymorphic superclass

This use case applies when the polymorphic superclass can be modified, and that polymorphism is intended to apply to all its subclasses.

This is done through the polymorphic_map option in the serialization blocks inside the polymorphic attribute class.

Syntax:

class PolymorphicSuperclass < Lutaml::Model::Serializable
  attribute :{_polymorphic_differentiator}, :string, polymorphic_class: true

  xml do
    (map_attribute | map_element) "XmlPolymorphicAttributeName", (1)
      to: :{_polymorphic_differentiator}, (2)
      polymorphic_map: { (3)
        "xml-value-for-subclass-1" => PolymorphicSubclass1, (4)
        "xml-value-for-subclass-2" => PolymorphicSubclass2,
      }
  end

  (key_value | key_value_format) do
    map "KeyValuePolymorphicAttributeName", (5)
      to: :{_polymorphic_differentiator}, (6)
      polymorphic_map: {
        "keyvalue-value-for-subclass-1" => PolymorphicSubclass1,
        "keyvalue-value-for-subclass-2" => PolymorphicSubclass2,
      }
  end
end

class PolymorphicSubclass1 < PolymorphicSuperclass
  # ...
end

class PolymorphicSubclass2 < PolymorphicSuperclass
  # ...
end

class PolymorphicAttributeClass < Lutaml::Model::Serializable
  attribute :polymorphic_attribute,
    PolymorphicSuperclass,
    {options},
    polymorphic: [
      PolymorphicSubclass1,
      PolymorphicSubclass2,
    ]
  # ...
end
1 The name of the XML element or attribute that contains the polymorphic differentiator.
2 The name of the polymorphic differentiator attribute defined in attribute with the polymorphic option.
3 The polymorphic_map option that determines the class to use based on the value of the differentiator.
4 The mapping of the differentiator value to the polymorphic subclass.
5 The name of the key-value element that contains the polymorphic differentiator.
6 The name of the polymorphic differentiator attribute defined in attribute with the polymorphic option.
class Reference < Lutaml::Model::Serializable
  attribute :_class, :string, polymorphic_class: true
  attribute :name, :string

  xml do
    map_attribute "reference-type", to: :_class, polymorphic_map: {
      "document-ref" => "DocumentReference",
      "anchor-ref" => "AnchorReference",
    }
    map_element "name", to: :name
  end

  key_value do
    map "_class", to: :_class, polymorphic_map: {
      "Document" => "DocumentReference",
      "Anchor" => "AnchorReference",
    }
    map "name", to: :name
  end
end

class DocumentReference < Reference
  attribute :document_id, :string

  xml do
    map_element "document_id", to: :document_id
  end

  key_value do
    map "document_id", to: :document_id
  end
end

class AnchorReference < Reference
  attribute :anchor_id, :string

  xml do
    map_element "anchor_id", to: :anchor_id
  end

  key_value do
    map "anchor_id", to: :anchor_id
  end
end

class ReferenceSet < Lutaml::Model::Serializable
  attribute :references, Reference, collection: true, polymorphic: [
    DocumentReference,
    AnchorReference,
  ]
end
---
references:
- _class: Document
  name: The Tibetan Book of the Dead
  document_id: book:tbtd
- _class: Anchor
  name: Chapter 1
  anchor_id: book:tbtd:anchor-1
<ReferenceSet>
  <references reference-type="document-ref">
    <name>The Tibetan Book of the Dead</name>
    <document_id>book:tbtd</document_id>
  </references>
  <references reference-type="anchor-ref">
    <name>Chapter 1</name>
    <anchor_id>book:tbtd:anchor-1</anchor_id>
  </references>
</ReferenceSet>
Mapping in the polymorphic attribute class and individual polymorphic subclasses

This use case applies when the polymorphic superclass is not meant to be modified.

This is done through the polymorphic_map option in the serialization blocks inside the polymorphic attribute class, and the polymorphic option in the individual polymorphic subclasses.

In this scenario, similar to the previous case where the polymorphic differentiator is set at the polymorphic superclass, the following conditions must be satisifed:

  • the polymorphic differentiator attribute name must be the same across polymorphic subclasses

    If the model polymorphic differentiator in one polymorphic subclass is _ref_type, then it must be so in all other polymorphic subclasses.

  • the polymorphic differentiator in the serialization formats must be identical within the polymorphic subclasses of that serialization format.

    If the XML polymorphic differentiator is reference-type, then it must be so in the XML of all polymorphic subclasses.

Syntax:

# Assume that we have no access to the base class and we need to define
# polymorphism in the sub-classes.
class PolymorphicSuperclass < Lutaml::Model::Serializable
end

class PolymorphicSubclass1 < PolymorphicSuperclass
  attribute :_polymorphic_differentiator, :string

  xml do
    (map_attribute | map_element) "XmlPolymorphicAttributeName", (1)
      to: :_polymorphic_differentiator
  end

  (key_value | key_value_format) do
    map "KeyValuePolymorphicAttributeName", (2)
      to: :_polymorphic_differentiator
  end
end

class PolymorphicSubclass2 < PolymorphicSuperclass
  attribute :_polymorphic_differentiator, :string

  xml do
    (map_attribute | map_element) "XmlPolymorphicAttributeName2",
      to: :_polymorphic_differentiator
  end

  (key_value | key_value_format) do
    map "KeyValuePolymorphicAttributeName2",
      to: :_polymorphic_differentiator
  end
end

class PolymorphicAttributeClass < Lutaml::Model::Serializable
  attribute :polymorphic_attribute,
    PolymorphicSuperclass,
    {options},
    polymorphic: [
      PolymorphicSubclass1,
      PolymorphicSubclass2,
    ] (3)
  # ...

  xml do
    map_element "XmlPolymorphicElement", (4)
      to: :polymorphic_attribute,
      polymorphic: { (5)
        # This refers to the polymorphic differentiator attribute in the polymorphic subclass.
        attribute: :_polymorphic_differentiator, (6)
        class_map: { (7)
          "xml-i-am-subclass-1" => "PolymorphicSubclass1",
          "xml-i-am-subclass-2" => "PolymorphicSubclass2",
        },
      }
  end

  (key_value | key_value_format) do
    map "KeyValuePolymorphicAttributeName", (8)
      to: :polymorphic_attribute,
      polymorphic: { (9)
        attribute: :_polymorphic_differentiator, (10)
        class_map: { (11)
          "keyvalue-i-am-subclass-1" => "PolymorphicSubclass1",
          "keyvalue-i-am-subclass-2" => "PolymorphicSubclass2",
        },
      }
  end

end
1 The name of the XML element or attribute that contains the polymorphic differentiator.
2 The name of the key-value element that contains the polymorphic differentiator.
3 Definition of the polymorphic attribute and the polymorphic subclasses in the polymorphic attribute class.
4 The name of the XML element that contains the polymorphic attributes. This must be an element as a polymorphic attribute must be a model.
5 The polymorphic option on a mapping defines necessary information for polymorphic serialization.
6 The attribute: name of the polymorphic differentiator attribute defined in the polymorphic subclass.
7 The class_map: option that determines the polymorphic subclass to use based on the value of the differentiator.
8 The name of the key-value format key that contains the polymorphic attributes.
9 Same as <5>, but for the key-value format.
10 Same as <6>, but for the key-value format.
11 Same as <7>, but for the key-value format.
class Reference < Lutaml::Model::Serializable
  attribute :name, :string
end

class DocumentReference < Reference
  attribute :_class, :string
  attribute :document_id, :string

  xml do
    map_element "document_id", to: :document_id
    map_attribute "reference-type", to: :_class
  end

  key_value do
    map "document_id", to: :document_id
    map "_class", to: :_class
  end
end

class AnchorReference < Reference
  attribute :_class, :string
  attribute :anchor_id, :string

  xml do
    map_element "anchor_id", to: :anchor_id
    map_attribute "reference-type", to: :_class
  end

  key_value do
    map "anchor_id", to: :anchor_id
    map "_class", to: :_class
  end
end

class ReferenceSet < Lutaml::Model::Serializable
  attribute :references, Reference, collection: true, polymorphic: [
    DocumentReference,
    AnchorReference,
  ]

  xml do
    root "ReferenceSet"

    map_element "reference", to: :references, polymorphic: {
      # This refers to the attribute in the polymorphic model, you need
      # to specify the attribute name (which is specified in the sub-classed model).
      attribute: "_class",
      class_map: {
        "document-ref" => "DocumentReference",
        "anchor-ref" => "AnchorReference",
      },
    }
  end

  key_value do
    map "references", to: :references, polymorphic: {
      attribute: "_class",
      class_map: {
        "Document" => "DocumentReference",
        "Anchor" => "AnchorReference",
      },
    }
  end
end
---
references:
- _class: Document
  name: The Tibetan Book of the Dead
  document_id: book:tbtd
- _class: Anchor
  name: Chapter 1
  anchor_id: book:tbtd:anchor-1
<ReferenceSet>
  <reference reference-type="document-ref">
    <name>The Tibetan Book of the Dead</name>
    <document_id>book:tbtd</document_id>
  </reference>
  <reference reference-type="anchor-ref">
    <name>Chapter 1</name>
    <anchor_id>book:tbtd:anchor-1</anchor_id>
  </reference>
</ReferenceSet>

Collection attributes

Define attributes as collections (arrays or hashes) to store multiple values using the collection option.

When defining a collection attribute, it is important to understand the default initialization behavior and how to customize it.

By default, collections are initialized as nil. However, if you want the collection to be initialized as an empty array, you can use the initialize_empty: true option.

collection can be set to:

true

The attribute contains an unbounded collection of objects of the declared class.

{min}..{max}

The attribute contains a collection of objects of the declared class with a count within the specified range. If the number of objects is out of this numbered range, a CollectionCountOutOfRangeError will be raised.

When set to 0..1, it means that the attribute is optional, it could be empty or contain one object of the declared class.

When set to 1.. (equivalent to 1..Infinity), it means that the attribute must contain at least one object of the declared class and can contain any number of objects.

When set to 5..10` means that there is a minimum of 5 and a maximum of 10 objects of the declared class. If the count of values for the attribute is less then 5 or greater then 10, the CollectionCountOutOfRangeError will be raised.

Syntax:

attribute :name_of_attribute, Type, collection: true
attribute :name_of_attribute, Type, collection: {min}..{max}
attribute :name_of_attribute, Type, collection: {min}..
Example 7. Using the collection option to define a collection attribute
class Studio < Lutaml::Model::Serializable
  attribute :location, :string
  attribute :potters, :string, collection: true
  attribute :address, :string, collection: 1..2
  attribute :hobbies, :string, collection: 0..
end
> Studio.new
> # address count is `0`, must be between 1 and 2  (Lutaml::Model::CollectionCountOutOfRangeError)
> Studio.new({ address: ["address 1", "address 2", "address 3"] })
> # address count is `3`, must be between 1 and 2  (Lutaml::Model::CollectionCountOutOfRangeError)
> Studio.new({ address: ["address 1"] }).potters
> # []
> Studio.new({ address: ["address 1"] }).address
> # ["address 1"]
> Studio.new(address: ["address 1"], potters: ['John Doe', 'Jane Doe']).potters
> # ['John Doe', 'Jane Doe']
# Default to `nil`
class SomeModel < Lutaml::Model::Serializable
  attribute :coll, :string, collection: true

  xml do
    root "some-model"
    map_element 'collection', to: :coll
  end

  key_value do
    map 'collection', to: coll
  end
end

puts SomeModel.new.coll
# => nil

puts SomeModel.new.to_xml
# =>
# <some-model xsi:xmlns="..."><collection xsi:nil="true"/></some-model>

puts SomeModel.new.to_yaml
# =>
# ---
# coll: null
# Default to empty array
class SomeModel < Lutaml::Model::Serializable
  attribute :coll, :string, collection: true, initialize_empty: true

  xml do
    map_element 'collection', to: :coll
  end

  key_value do
    map 'collection', to: coll
  end
end

puts SomeModel.new.coll
# => []

puts SomeModel.new.to_xml
# =>
# <some-model><collection/></some-model>

puts SomeModel.new.to_yaml
# =>
# ---
# coll: []

Derived attributes

A derived attribute has a value computed dynamically on evaluation of an instance method.

It is defined using the method: option along with a mandatory type specification. If the return value is not of the type, it will be casted to the specified type.

Syntax:

attribute :name_of_attribute, Type, method: :instance_method_name
Example 8. Defining methods as attributes
class Invoice < Lutaml::Model::Serializable
  attribute :subtotal, :float
  attribute :tax, :float
  attribute :total, :float, method: :total_value

  def total_value
    subtotal + tax
  end
end

i = Invoice.new(subtotal: 100.0, tax: 12.0)
i.total
#=> 112.0

puts i.to_yaml
#=> ---
#=> subtotal: 100.0
#=> tax: 12.0
#=> total: 112.0

Choice attributes

The choice directive allows specifying that elements from the specified range are included.

Attribute-level definitions are supported. This can be used with both key_value and xml mappings.

Syntax:

choice(min: {min}, max: {max}) do
  {block}
end

Where,

min

The minimum number of elements that must be included. The minimum value can be 0.

max

The maximum number of elements that can be included. The maximum value can go up to Float::INFINITY.

block

The block of elements that must be included. The block can contain multiple attribute and choice directives.

Example 9. Using the choice directive to define a set of attributes with a range
class Studio < Lutaml::Model::Serializable
  choice(min: 1, max: 3) do
    choice(min: 1, max: 2) do
      attribute :prefix, :string
      attribute :forename, :string
    end

    attribute :completeName, :string
  end
end

This means that the Studio class must have at least one and at most three attributes.

  • The first choice must have at least one and at most two attributes.

  • The second attribute is the completeName.

  • The first choice can have either the prefix and forename attributes or just the forename attribute.

  • The last attribute completeName is optional.

Choice and sequence can be used together to create complex structures.

Example 10. Using choice (model-level) and sequence (XML-level) directives together
class Person < Lutaml::Model::Serializable
  attribute :first_name, :string
  attribute :last_name, :string
  choice do
    attribute :age, :integer
    attribute :dob, :string
  end
  choice(min: 1, max: 2) do
    attribute :email, :string, collection: 0..2
    attribute :phone, :string, collection: 0..2
    attribute :address, :string, collection: true
  end

  xml do
    root "Person", mixed: true
    sequence do
      map_element :FirstName, to: :first_name
      map_element :LastName, to: :last_name
      map_element :Age, to: :age
      map_element :Dob, to: :dob
      map_element :Email, to: :email
      map_element :Phone, to: :phone
      map_element :Address, to: :address
    end
  end
end

Choosing between Custom Types and Transform Procs

When deciding how to implement value transformations, consider:

Use Custom Value Type classes when:

  • Bidirectional transformations are needed across formats

  • Format-specific representations are required (XML wants YYYYMMDD, JSON wants DDMMYYYY)

  • The logic will be reused across multiple attributes or models

  • Complex parsing or calculation is involved (e.g., ISO week date calculations)

  • Type safety and encapsulation are important

Use Attribute-level transform procs when:

  • Same simple transformation applies to ALL serialization formats uniformly

  • Logic is specific to one attribute in one model (non-reusable)

  • Quick inline modification is sufficient

  • No format-specific behavior is needed

Use Mapping-level transform procs when:

  • Different transformation needed per serialization format

  • One-off, non-reusable transformation

  • Combined with attribute-level transforms for multi-stage processing

See Value Transformations Guide for complete decision matrix and examples.

The choice directive can be used with import_model_attributes. For more details, see Using import_model_attributes inside a choice block.