General

Collections are used to represent a contained group of multiple instances of models.

Typically, a collection represents an "Array" or a "Set" in information modeling and programming languages. In LutaML, a collection represents an array of model instances.

Models in a collection may be:

  • constrained to be of a single kind;

  • constrained to be of multiple kinds sharing common characteristics;

  • unbounded of any kind.

LutaML Model provides the Lutaml::Model::Collection class for defining collections of model instances.

Configuration

All formats

The instances directive defined at the Collection class level is used to define the collection attribute and the model type of the collection elements.

Syntax:

class MyCollection < Lutaml::Model::Collection
  instances {attribute}, {ModelType}
end

Where,

attribute

The name of the attribute that contains the collection.

ModelType

The model type of the collection elements.

Mapping instances: key-value formats only

The map_instances directive is only used in the key_value block.

Syntax:

class MyCollection < Lutaml::Model::Collection
  instances {attribute}, ModelType

  key_value do
    map_instances to: {attribute}
  end
end

Where,

attribute

The name of the attribute that contains model instances.

This directive maps individual array elements to the defined instances attribute. These are the items considered part of the Collection and reflected as Enumerable elements.

Mapping instances: XML only

In the xml block, the map_element, map_attribute directives are used instead.

These directives map individual array elements to the defined instances attribute. These are the items considered part of the Collection and reflected as Enumerable elements.

Syntax for an element collection:

class MyCollection < Lutaml::Model::Collection
  instances {attribute}, ModelType

  xml do
    map_element "element-name", to: {attribute}
  end
end

Where,

element-name

The name of the XML element of each model instance.

Syntax for an attribute collection:

class MyCollection < Lutaml::Model::Collection
  instances {attribute}, ModelType
  xml do
    map_attribute "attribute-name", to: {attribute}
  end
end

Where,

attribute-name

The name of the XML attribute that contains all model instances.

XML attribute collections with delimited values

Lutaml::Model supports handling collections realized as XML attributes.

This feature allows you to serialize and deserialize multiple values stored in a single XML attribute, separated by a delimiter.

There are two approaches for handling delimited attribute values:

  • Using the delimiter: option: provides simple splitting and joining with a fixed delimiter;

  • Using the as_list: option: provides custom import and export logic.

The delimiter option is a straightforward way to handle attribute values that are delimited strings. It is ideal for simple cases where you need basic string splitting and joining with a fixed delimiter. It automatically splits the string during import and joins the array during export.

class TitleDelimiterCollection < Lutaml::Model::Collection
  instances :items, :string

  xml do
    root "titles"
    map_attribute "title", to: :items, delimiter: "; " (1)
  end
end
1 The delimiter used to split and join the string values.

The as_list option provides full control over how values are split during import and joined during export. This is useful when you need custom parsing logic. This allows for more complex transformations beyond simple splitting and joining that is achieved through using delimiters.

class TitleCollection < Lutaml::Model::Collection
  instances :items, :string

  xml do
    root "titles"
    map_attribute "title", to: :items, as_list: {
      import: ->(str) { str.split("; ") }, (1)
      export: ->(arr) { arr.join("; ") }, (2)
    }
  end
end
1 Custom logic to split the string into an array during import.
2 Custom logic to join the array into a string during export.
Example 1. Applying the delimiter and as_list options for XML attribute collections

Both approaches work with the same XML format:

<titles title="Title One; Title Two; Title Three"/>
# Both collections work identically for basic use cases
collection = TitleCollection.from_xml(xml)
collection.items # => ["Title One", "Title Two", "Title Three"]
collection.to_xml # => '<titles title="Title One; Title Two; Title Three"/>'

# Same result with delimiter option
delimiter_collection = TitleDelimiterCollection.from_xml(xml)
delimiter_collection.items # => ["Title One", "Title Two", "Title Three"]
delimiter_collection.to_xml # => '<titles title="Title One; Title Two; Title Three"/>'

Collection types

A LutaML collections is used for a number of scenarios:

  • Root collections (for key-value formats)

  • Named collections

  • Keyed collections (for key-value formats)

  • Attribute collections

  • Nested collections

Root collections (key-value formats only)

General

TODO: This case needs to be fixed for JSON.

A root collection is a collection that is not contained within a parent collection.

Root collections only apply to key-value serialization formats. The XML format does not support root collections.

The XML standard mandates the existence of a non-repeated "root element" in an XML document. This means that a valid XML document must have a root element, and all elements in an XML document must exist within the root. This is why an XML document cannot be a "root collection".
A root collection cannot be represented using a non-collection model.

Root collections store multiple instances of the same model type at the root level. In other words, these are model instances that do not have a defined container at the level of the LutaML Model.

There are two kinds of root collections depending on the type of the instance value:

"Root value collection"

the value is a "primitive type"

"Root object collection"

the value is a "model instance"

Regardless of the type of root collection, the instance in a collection is always a LutaML model instance.

Root value collections

A root value collection is a collection that directly contains values of a primitive type.

Example 2. Simple root collection with each instance being a value
---
- Item One
- Item Two
- Item Three
[
  "Item One",
  "Item Two",
  "Item Three"
]

Syntax:

class MyCollection < Lutaml::Model::Collection
  instances :items, ModelType
end

class ModelType < Lutaml::Model::Serializable
  attribute :name, :string
end
Example 3. Handling a root collection where each instance is a value

Code:

class Title < Lutaml::Model::Serializable
  attribute :content, :string
end

class TitleCollection < Lutaml::Model::Collection
  instances :titles, Title

  key_value do
    no_root # default
    map_instances to: :titles
  end
end

Data:

---
- Title One
- Title Two
- Title Three
[
  "Title One",
  "Title Two",
  "Title Three"
]

Usage:

titles = TitleCollection.from_yaml(yaml_data)
titles.count
# => 3
titles.first.content
# => "Title One"

Root object collections

A root object collection is a collection that directly contains model instances, each containing at least one serialized attribute.

Example 4. Simple root collection in YAML with each instance being a models with an attribute name
---
- name: Item One
- name: Item Two
- name: Item Three
[
  {"name": "Item One"},
  {"name": "Item Two"},
  {"name": "Item Three"}
]
Example 5. Handling a root collection where each instance is defined by a model with attributes

Code:

class Title < Lutaml::Model::Serializable
  attribute :content, :string
end

class TitleCollection < Lutaml::Model::Collection
  instances :titles, Title

  key_value do
    no_root # default
    map_instances to: :titles
  end
end

Data:

---
- content: Title One
- content: Title Two
- content: Title Three
[
  {"content": "Title One"},
  {"content": "Title Two"},
  {"content": "Title Three"}
]

Usage:

titles = TitleCollection.from_yaml(yaml_data)
titles.count
# => 3
titles.first.content
# => "Title One"

Named collections

General

Named collections are collections wrapped inside a name or a key. The "name" of the collection serves as the container root of its contained model instances.

The named collection setup applies to XML and key-value serialization formats.

In a named collection setup, the collection is defined as a Lutaml::Model::Collection class, and each instance is defined as a Lutaml::Model::Serializable class.

There are two kinds of named collections depending on the type of the instance value:

"Named value collection"

the value is a "primitive type"

"Named object collection"

the value is a "model instance"

Regardless of the name of root collection, the instance in a collection is always a LutaML model instance.

Named value collections

A named value collection is a collection that contains values of a primitive type.

Named value collection in XML with models each containing an element with content
<names>
  <name>Item One</name>
  <name>Item Two</name>
  <name>Item Three</name>
</names>
Named value collection in YAML with models each containing a value
---
names:
- Item One
- Item Two
- Item Three

Syntax:

class MyCollection < Lutaml::Model::Collection
  instances :items, ModelType

  xml do
    root "name-of-xml-container-element"
  end

  key_value do
    root "name-of-key-value-container-element"
  end
end

class ModelType < Lutaml::Model::Serializable
  attribute :name, :string
end

A named collection can alternatively be implemented as a non-collection model ("Model class with an attribute") that contains the collection of instances. In this case, the attribute will be an Array object, which does not contain additional attributes and methods.

Example 6. Handling a named collection with instance elements directly containing values
class Title < Lutaml::Model::Serializable
  attribute :title, :string

  xml do
    root "title"
    map_content to: :title
  end
end

class DirectTitleCollection < Lutaml::Model::Collection
  instances :items, Title

  xml do
    root "titles"
    map_element "title", to: :items
  end

  key_value do
    map_instances to: :items
  end
end
<titles>
  <title>Title One</title>
  <title>Title Two</title>
  <title>Title Three</title>
</titles>
---
titles:
- Title One
- Title Two
- Title Three
{
  "titles": [
    "Title One",
    "Title Two",
    "Title Three"
  ]
}
titles = DirectTitleCollection.from_yaml(yaml_data)
titles.count
# => 3
titles.first.title
# => "Title One"
titles.last.title
# => "Title Three"

Named object collections

A named object collection is a collection that contains model instances, each containing at least one serialized attribute.

A named object collection can alternatively be implemented as a non-collection model ("Model class with an attribute") that contains the collection of instances. In this case, the attribute will be an Array object, which does not contain additional attributes and methods.
Named object collection in XML with instances each containing an element with a model attribute
<names>
  <name><content>Item One</content></name>
  <name><content>Item Two</content></name>
  <name><content>Item Three</content></name>
</names>
Named object collection in YAML with instances each containing a model attribute
---
names:
- name: Item One
- name: Item Two
- name: Item Three
Example 7. Named object collection with each instance containing at least one model attribute

Data:

<titles>
  <title><content>Title One</content></title>
  <title><content>Title Two</content></title>
  <title><content>Title Three</content></title>
</titles>
---
titles:
- title: Title One
- title: Title Two
- title: Title Three
{
  "titles": [
    {"title": "Title One"},
    {"title": "Title Two"},
    {"title": "Title Three"}
  ]
}

Code:

class Title < Lutaml::Model::Serializable
  attribute :title, :string

  xml do
    root "title"
    map_element "content", to: :title
  end

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

class TitleCollection < Lutaml::Model::Collection
  instances :items, Title

  xml do
    root "titles"
    map_element 'title', to: :items
  end

  key_value do
    root "titles"
    map_instances to: :items
  end
end

Usage:

titles = TitleCollection.from_yaml(yaml_data)
titles.count
# => 3
titles.first.title
# => "Title One"
titles.last.title
# => "Title Three"

Attribute collection class

A model attribute that is a collection can be contained within a custom collection class.

A custom collection class can be defined to provide custom behavior for the collection inside a non-collection model, with attributes using collection: true.

Syntax:

class MyModel < Lutaml::Model::Serializable
  attribute {model-attribute}, ModelType, collection: MyCollection
end

class MyCollection < Lutaml::Model::Collection
  instances {instance-name}, ModelType

  # Custom behavior for the collection
  def custom_method
    # Custom logic here
  end
end
Example 8. Using a custom collection class for custom collection behavior

Data:

<titles>
  <title>Title One</title>
  <title>Title Two</title>
  <title>Title Three</title>
</titles>
titles:
- title: Title One
- title: Title Two
- title: Title Three
class StringParts < Lutaml::Model::Collection
  instances :parts, :string

  def to_s
    parts.join(' -- ')
  end
end

class BibliographicItem < Lutaml::Model::Serializable
  attribute :title_parts, :string, collection: StringParts

  xml do
    root "titles"
    map_element "title", to: :title_parts
  end

  key_value do
    root "titles"
    map_instances to: :title_parts
  end

  def render_title
    title_parts.to_s
  end
end
> bib_item = BibliographicItem.from_xml(xml_data)
> bib_item.title_parts
> # StringParts:0x0000000104ac7240 @parts=["Title One", "Title Two", "Title Three"]
> bib_item.render_title
> # "Title One -- Title Two -- Title Three"

Nested collections

TODO: This case needs to be fixed.

Collections can be nested within other models and define their own serialization rules.

Nested collections can be defined in the same way as root collections, but they are defined within the context of a parent model.

Data:

<titles>
  <title-group>
    <artifact>
      <content>Title One</content>
    </artifact>
    <artifact>
      <content>Title Two</content>
    </artifact>
    <artifact>
      <content>Title Three</content>
    </artifact>
  </title-group>
</titles>
---
titles:
  title-group:
    - artifact:
        content: Title One
    - artifact:
        content: Title Two
    - artifact:
        content: Title Three
class Title < Lutaml::Model::Serializable
  attribute :content, :string
end

class TitleCollection < Lutaml::Model::Collection
  instances :items, Title

  xml do
    root "title-group"
    map_element "artifact", to: :items
  end
end

class BibItem < Lutaml::Model::Serializable
  attribute :titles, TitleCollection

  xml do
    root "bibitem"
    # This overrides the collection's root "title-group"
    map_element "titles", to: :titles
  end
end

Keyed collections (key-value serialization formats only)

General

In key-value serialization formats, a key can be used to uniquely identify each instance. This usage allows for enforcing uniqueness in the collection.

A collection that contains keyed objects as its instances is commonly called a "keyed collection". A keyed object in a serialization format is an object identified with a unique key.

The concept of keyed collections does not typically apply to XML collections.

There are two kinds of keyed collections depending on the type of the keyed value:

"keyed value collection"

the value is a "primitive type"

"keyed object collection"

the value is a "model instance"

Regardless of the type of keyed collections, the instance in a collection is always a LutaML model instance.

map_key method

The map_key method specifies that the unique key is to be moved into an attribute belonging to the instance model.

Syntax:

key_value do
  map_key to_instance: {instance-attribute-name}
end

Where,

to_instance

Refers to the attribute name in the instance that contains the key.

{key_attribute}

The attribute name in the instance that contains the key.

map_value method

The map_value method specifies that the value (the object referenced by the unique key) is to be moved into an attribute belonging to the instance model.

Syntax:

key_value do
  # basic pattern
  map_value {operation}: [*argument]

  # Mapping the value object to a full instance through `to_instance`
  map_value to_instance: {instance-attribute-name}

  # Mapping the value object to an attribute as_instance
  map_value as_attribute: {instance-attribute-name}
end

Keyed value collections

A keyed value collection is a collection where the keyed item in the serialization format is a primitive type (e.g. string, integer, etc.).

The instance item inside the collection is a model instance that contains both the serialized key and serialized value both as attributes inside the model.

All three map_key, map_value, and map_instances methods need to be used to define how instances are mapped in a keyed value collection.

Example 9. Creating a keyed value collection
class AuthorAvailability < Lutaml::Model::Serializable
  attribute :id, :string
  attribute :available, :boolean
end

class AuthorCollection < Lutaml::Model::Collection
  instances :authors, AuthorAvailability

  key_value do
    map_key to_instance: :id # This refers to 'authors[].id'
    map_value as_attribute: :available # This refers to 'authors[].available'
    map_instances to: :authors
  end
end
---
author_01: true
author_02: false
author_03: true
authors = AuthorCollection.from_yaml(yaml_data)
authors.first.id
# => "author_01"
authors.first.available
# => true

Keyed object collections

A keyed object collection is a collection where the keyed item in the serialization format contains multiple attributes.

The instance item inside the collection is a model instance that contains the serialized key as one attribute, and the serialized value attributes are all attributes inside the model.

Both the map_key and map_instances are used to define how instances are mapped in a keyed object collection.

Example 10. Creating a keyed object collection
class Author < Lutaml::Model::Serializable
  attribute :id, :string
  attribute :name, :string
end

class AuthorCollection < Lutaml::Model::Collection
  instances :authors, Author

  key_value do
    map_key to_instance: :id # This refers to 'authors[].id'
    map_instances to: :authors
  end
end
---
author_01:
  name: Author One
author_02:
  name: Author Two
author_03:
  name: Author Three
authors = AuthorCollection.from_yaml(yaml_data)
authors.first.id
# => "author_01"
authors.first.name
# => "Author One"

Nested keyed object collection

A nested keyed object collection is a keyed collection that contain other keyed collections. This case is simply a more complex arrangement of the principles applied to keyed object collections.

This pattern can extend to multiple levels of nesting, where each level contains a keyed object collection that can have its own key and value mappings.

Depends on whether a custom collection class is needed, the following mechanisms are available:

  • When using a Lutaml::Model::Serializable class for a keyed collection, use the child_mappings option to map attributes.

  • When using a Lutaml::Model::Collection class for a keyed collection, there are two options:

  • use the map_key, map_value, and map_instances methods to map attributes; or

  • use the root_mappings option to map attributes.

Example 11. Nested 2-layer keyed object collection

This example provides a two-layer nested structure where:

  • The first layer keys pieces by type (bowls, vases).

  • The second layer keys glazes by finish name within each piece type.

  • Each glaze finish contains detailed attributes like temperature.

# Third layer represents glaze finishes.
class GlazeFinish < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :temperature, :integer

  key_value do
    map "name", to: :name
    map "temperature", to: :temperature
  end
end

# Second layer represents ceramic pieces each with multiple finishes.
class CeramicPiece < Lutaml::Model::Serializable
  attribute :piece_type, :string
  attribute :glazes, GlazeFinish, collection: true

  key_value do
    map "piece_type", to: :piece_type
    map "glazes", to: :glazes, child_mappings: {
      name: :key,
      temperature: :temperature
    }
  end
end

# Uppermost layer represents the collection of ceramic pieces.
class StudioInventory < Lutaml::Model::Collection
  instances :pieces, CeramicPiece

  key_value do
    map to: :pieces, root_mappings: {
      piece_type: :key,
      glazes: :value,
    }
  end
end
---
bowls:
  matte_finish:
    name: Earth Matte
    temperature: 1240
  glossy_finish:
    name: Ocean Blue
    temperature: 1260
  crackle_finish:
    name: Antique Crackle
    temperature: 1220
vases:
  metallic_finish:
    name: Bronze Metallic
    temperature: 1280
  crystalline_finish:
    name: Ice Crystal
    temperature: 1300
inventory = StudioInventory.from_yaml(yaml_data)

# Access nested data through the hierarchy
puts inventory.pieces.bowls.matte_finish.name
# => "Earth Matte"

puts inventory.pieces.bowls.matte_finish.temperature
# => 1240

# Iterate through all pieces and their glazes
inventory.pieces.each do |piece_type, piece|
  puts "#{piece_type.capitalize}:"
  piece.glazes.each do |glaze_name, glaze|
    puts "  #{glaze_name}: #{glaze.name} (#{glaze.temperature}°C)"
  end
end

Behavior

Enumerable interface

Collections implement the Ruby Enumerable interface, providing standard collection operations.

Collections allow the following sample Enumerable methods:

  • each - Iterate over collection items

  • map - Transform collection items

  • select - Filter collection items

  • find - Find items matching criteria

  • reduce - Aggregate collection items

Example 12. Usage of the collection Enumerable interface
# Filter items
filtered = collection.filter { |item| item.id == "1" }

# Reject items
rejected = collection.reject { |item| item.id == "1" }

# Select items
selected = collection.select { |item| item.id == "1" }

# Map items
mapped = collection.map { |item| item.name }

# Count items
count = collection.count

Initialization

Collections can be initialized with an array of items or through individual item addition.

# Empty collection
collection = ItemCollection.new

# From an array of items
collection = ItemCollection.new([item1, item2, item3])

# From an array of hashes
collection = ItemCollection.new([
  { id: "1", name: "Item 1" },
  { id: "2", name: "Item 2" }
])

# Adding items later
collection << Item.new(id: "3", name: "Item 3")

Collection mutation

Collection attributes can be mutated after initialization using standard Ruby array methods or custom helper methods. These mutations are properly reflected during serialization.

When a collection attribute is defined with a default value (e.g., default: → { [] }), you can mutate it directly using Ruby’s array methods such as <<, push, concat, etc., or through custom methods that add items to the collection.

Example 13. Mutating collection attributes after initialization
class TabStop < Lutaml::Model::Serializable
  attribute :position, :integer
  attribute :alignment, :string

  xml do
    element 'tab'
    map_attribute 'pos', to: :position
    map_attribute 'val', to: :alignment
  end
end

class TabStopCollection < Lutaml::Model::Serializable
  attribute :tabs, TabStop, collection: true, default: -> { [] }

  xml do
    element 'tabs'
    map_element 'tab', to: :tabs
  end

  # Custom helper method for adding tabs
  def add_tab(position, alignment)
    tabs << TabStop.new(position: position, alignment: alignment)
  end
end

# Create collection with default empty array
collection = TabStopCollection.new

# Mutate using << operator
collection.tabs << TabStop.new(position: 1440, alignment: "center")

# Mutate using custom method
collection.add_tab(2880, "right")

# Mutations are properly serialized
puts collection.to_xml
# <tabs>
#   <tab pos="1440" val="center"/>
#   <tab pos="2880" val="right"/>
# </tabs>

This behavior enables natural Ruby idioms for working with collections:

  • Direct mutation: Use <<, push, pop, shift, unshift, concat, etc.

  • Custom methods: Create domain-specific helper methods like add_item, remove_item

  • No recreation needed: No need to recreate parent objects to update collections

Empty collections initialized with default values are not serialized unless items are added. This preserves the expected behavior where default values are only rendered when explicitly requested with render_default: true.

Ordering

TODO: This case needs to be fixed.

Collections that maintain a specific ordering of elements.

Syntax:

class MyCollection < Lutaml::Model::Collection
  instances {instances-name}, ModelType
  ordered by: {attribute-of-instance-or-proc}, order: {:asc | :desc}
end

Where,

{instances-name}

name of the instances accessor within the collection

ModelType

The model type of the collection elements.

{attribute-of-instance-or-proc}

How model instances are to be ordered by. Values supported are:

{attribute-of-instance}

Attribute name of an instance to be ordered by.

{proc}

Proc that returns a value to order by (same as sort_by), given the instance as input.

order

Order direction of the value:

:asc

Ascending order (default).

:desc

Descending order.

When a proc is provided for ordering and order: :desc is specified, the collection is first sorted using the proc (as with Ruby’s sort_by), and the resulting array is then reversed to achieve descending order.
Example 14. Ordered collection applied to a root collection

Data:

<items>
  <item id="3" name="Item Three"/>
  <item id="1" name="Item One"/>
  <item id="2" name="Item Two"/>
</items>
---
- id: 3
  name: Item Three
- id: 1
  name: Item One
- id: 2
  name: Item Two
class Item < Lutaml::Model::Serializable
  attribute :id, :string
  attribute :name, :string

  xml do
    map_attribute "id", to: :id
    map_attribute "name", to: :name
  end
end

class OrderedItemCollection < Lutaml::Model::Collection
  instances :items, Item
  ordered by: :id, order: :desc

  xml do
    root "items"
    map_element "item", to: :items
  end

  key_value do
    no_root
    map_instances to: :items
  end
end
> collection = OrderedItemCollection.from_xml(xml_data)
> collection.map(&:id)
> # ["3", "2", "1"]

> collection = OrderedItemCollection.from_yaml(yaml_data)
> collection.map(&:id)
> # ["3", "2", "1"]
Example 15. Ordered collection with proc-based ordering
class ProcOrderedItemCollection < Lutaml::Model::Collection
  instances :items, Item
  # Multi-level ordering: first by name length, then by name alphabetically
  ordered by: ->(item) { [item.name.length, item.name] }, order: :asc

  xml do
    root "items"
    map_element "item", to: :items
  end
end
> items_data = [
>   { id: "1", name: "Zebra" },
>   { id: "2", name: "Alpha" },
>   { id: "3", name: "Beta" }
> ]

> complex_collection = ProcOrderedItemCollection.new(items_data)
> complex_collection.map(&:name)
> # ["Beta", "Alpha", "Zebra"] # Sorted by name length then alphabetically

Indexed lookups

Collections can be indexed for O(1) lookups by one or more attributes. This is particularly useful for large collections where repeated lookups by key would otherwise be O(n).

Single index

Use index_by with a single field for simple indexed lookups:

class Person < Lutaml::Model::Serializable
  attribute :id, :string
  attribute :name, :string
end

class PersonCollection < Lutaml::Model::Collection
  instances :people, Person
  index_by :id
end

people = PersonCollection.new([
  { id: '001', name: 'Alice' },
  { id: '002', name: 'Bob' }
])

# O(1) lookup by id
people.fetch('001')              # => #<Person id='001' name='Alice'>
people.find_by(:id, '002')       # => #<Person id='002' name='Bob'>
people.find_by(:id, 'missing')   # => nil
Multiple indexes

Use index_by with multiple fields for lookups by different keys:

class PersonCollection < Lutaml::Model::Collection
  instances :people, Person
  index_by :id, :email, :slug
end

# O(1) lookup by any indexed field
people.find_by(:id, '001')
people.find_by(:email, 'alice@example.com')
people.find_by(:slug, 'alice-smith')
Named indexes with custom key extraction

Use index with a name and proc for custom key extraction:

class PersonCollection < Lutaml::Model::Collection
  instances :people, Person
  index_by :id
  index :email, by: ->(person) { person.email.downcase }
end

# Lookup with normalized key (proc stores lowercase)
people.find_by(:email, 'alice@example.com')  # => finds Alice
Combined with sorting

Indexes work alongside sorting:

class PersonCollection < Lutaml::Model::Collection
  instances :people, Person
  index_by :id
  sort by: :name, order: :asc
end

# Collection is sorted by name, indexed by id
people.fetch('001')
The index is rebuilt after any mutation (<<, push, []=). For best performance with large collections, batch mutations before lookups.

Polymorphic collections

Collections can contain instances of different model classes that share a common base class.

The polymorphic option for instances allows you to specify which subclasses are accepted:

class ReferenceSet < Lutaml::Model::Collection
  # Accepts any subclass of Reference
  instances :references, Reference, polymorphic: true
end

class ReferenceSet < Lutaml::Model::Collection
  # Accepts only DocumentReference and AnchorReference
  instances :references, Reference, polymorphic: [
    DocumentReference,
    AnchorReference,
  ]
end

Polymorphic collection mapping

To serialize/deserialize polymorphic collections, use the polymorphic option in your mapping blocks. This allows you to specify how to differentiate subclasses in XML, YAML, or JSON.

class ReferenceSet < Lutaml::Model::Collection
  instances :references, Reference, polymorphic: [
    DocumentReference,
    AnchorReference,
  ]

  xml do
    root "ReferenceSet"
    map_instances to: :references, polymorphic: {
      attribute: "_class",
      class_map: {
        "document-ref" => "DocumentReference",
        "anchor-ref" => "AnchorReference",
      },
    }
  end

  key_value do
    map_instances to: :references, polymorphic: {
      attribute: "_class",
      class_map: {
        "Document" => "DocumentReference",
        "Anchor" => "AnchorReference",
      },
    }
  end
end

This allows round-trip serialization like:

---
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>

Polymorphic mapping with subclass differentiator

If you cannot modify the polymorphic superclass, define the differentiator attribute in each subclass, and use the polymorphic mapping option in the collection:

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::Collection
  instances :references, Reference, polymorphic: [
    DocumentReference,
    AnchorReference,
  ]

  xml do
    root "ReferenceSet"
    map_instances to: :references, polymorphic: {
      attribute: "_class",
      class_map: {
        "document-ref" => "DocumentReference",
        "anchor-ref" => "AnchorReference",
      },
    }
  end

  key_value do
    map_instances to: :references, polymorphic: {
      attribute: "_class",
      class_map: {
        "Document" => "DocumentReference",
        "Anchor" => "AnchorReference",
      },
    }
  end
end