Skip to content

LutaML Model is an information modeler. Supports declarative definitions of serializations and transforms for models.

License

Notifications You must be signed in to change notification settings

lutaml/lutaml-model

Repository files navigation

LutaML Ruby modeller

RubyGems Version License Build Dependent tests

Purpose

Lutaml::Model is the Ruby implementation of the LutaML modeling methodology, for:

  • creating information models in the LutaML language (or its Ruby DSL)

  • serializing and deserializing LutaML information models

  • accessing data instances of LutaML information models

  • documenting LutaML information models

It provides simple, flexible and comprehensive mechanisms for defining information models with attributes and types, and the serialization of them to/from serialization formats including Hash, JSON, XML, YAML, and TOML.

For serialization formats, it uses an adapter pattern to support multiple libraries for each format, providing flexibility and extensibility for your data modeling needs.

Note
The Lutaml::Model modeling Ruby DSL was originally designed to be mostly compatible with the data modeling DSL of Shale, a data modeller for Ruby. Lutaml::Model is meant to address advanced needs not currently addressed by Shale. Instructions on how to migrate from Shale to Lutaml::Model are provided in Migration steps from Shale.

Features

  • Define models with attributes and types

  • Serialize and deserialize models to/from Hash, JSON, XML, YAML, and TOML

  • Support for multiple serialization libraries (e.g., toml-rb, tomlib)

  • Configurable adapters for different serialization formats

  • Support for collections and default values

  • Custom serialization/deserialization methods

  • XML namespaces and mappings

  • Create custom adapters for additional data formats (see Custom Adapters)

Data modeling in a nutshell

Data modeling is the process of creating a data model for the data to be stored in a database or used in an application. It helps in defining the structure, relationships, and constraints of the data, making it easier to manage and use.

Lutaml::Model simplifies data modeling in Ruby by allowing you to define models with attributes and serialize/deserialize them to/from various serialization formats seamlessly.

The Lutaml::Model data modelling approach is as follows:

Modeling relationships of a LutaML Model
                       LutaML Model
                             │
                    Has many attributes
                             │
                             ▼
                         Attribute
                             │
                        Has type of
                             │
                  ┌──────────┴──────────┐
                  │                     │
                Model              Value (Leaf)
                  │                     │
       Has many attributes    Contains one basic value
                  │                     │
          ┌───────┴─────┐        ┌──────┴──────┐
          │             │        │             │
        Model      Value (Leaf)  String       Integer
          │                      Date         Boolean
          │                      Time         Float
  Has many attributes            ...          ...
          │
          ▼
  (Recursive pattern continues...)
Example 1. Example of LutaML Model instance with assigned values
Studio (Model)
 ├── name (Value: String) = "Pottery Studio"
 ├── address (Model)
 │    ├── street (Value: String) = "123 Clay St"
 │    ├── city (Value: String) = "Ceramics City"
 │    └── postcode (Value: String) = "12345"
 ├── established (Value: Date) = 2020-01-01
 └── kilns (Model)
      ├── count (Value: Integer) = 3
      └── temperature (Value: Float) = 1200.0
Modeling relationships of a LutaML Model to serialization models
╔═══════════════════════╗                       ╔════════════════════════════╗
║   LutaML Core Model   ║                       ║    Serialization Models    ║
╚═══════════════════════╝                       ╚════════════════════════════╝

╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮                       ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
┆          Model        ┆                       ┆          XML Model         ┆
┆            │          ┆   ┌────────────────┐  ┆              │             ┆
┆   ┌────────┴──┐       ┆   │                │  ┆       ┌──────┴──────┐      ┆
┆   │           │       ┆   │     Model      │  ┆       │             │      ┆
┆ Models   Value Types  ┆──►│ Transformation │  ┆    Models      Value Types ┆
┆   │           │       ┆   │       &        │  ┆       │             │      ┆
┆   │           │       ┆   │ Mapping Rules  │  ┆       │             │      ┆
┆   │    ┌──────┴──┐    ┆   │                │  ┆  ┌────┴────┐      ┌─┴─┐    ┆
┆   │    │         │    ┆   └────────────────┘  ┆  │         │      │   │    ┆
┆   │   String  Integer ┆           │           ┆ Element  Value  xs:string  ┆
┆   │   Date    Float   ┆           │           ┆ Attribute Type  xs:date    ┆
┆   │   Time    Boolean ┆           ├──────────►┆                 xs:boolean ┆
┆   │                   ┆           │           ┆                 xs:anyURI  ┆
┆   └──────┐            ┆           │           ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
┆          │            ┆           │
┆     Contains          ┆           │           ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
┆     more Models       ┆           │           ┆          JSON Model        ┆
┆     (recursive)       ┆           │           ┆              │             ┆
┆                       ┆           │           ┆       ┌──────┴──────┐      ┆
╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯           └──────────►┆       │             │      ┆
                                                ┆    Models      Value Types ┆
                                                ┆       │             │      ┆
                                                ┆       │             │      ┆
                                                ┆  ┌────┴───┐     ┌───┴──┐   ┆
                                                ┆  │        │     │      │   ┆
                                                ┆ object array number string ┆
                                                ┆ value        boolean null  ┆
                                               ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
Model transformation of a LutaML Model to another LutaML Model
╔═══════════════════════╗   ╔══════════════════╗   ╔═══════════════════════╗
║LutaML Model Class FOO ║   ║LutaML Transformer║   ║LutaML Model Class BAR ║
╚═══════════════════════╝   ╚══════════════════╝   ╚═══════════════════════╝

╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮                          ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
┆          Model        ┆                          ┆          Model        ┆
┆            │          ┆    ┌────────────────┐    ┆            │          ┆
┆   ┌────────┴──┐       ┆    │                │    ┆   ┌────────┴──┐       ┆
┆   │           │       ┆    │     Model      │    ┆   │           │       ┆
┆ Models   Value Types  ┆───►│ Transformation │───►┆ Models   Value Types  ┆
┆   │           │       ┆◄───│       &        │◄───┆   │           │       ┆
┆   │           │       ┆    │ Mapping Rules  │    ┆   │           │       ┆
┆   │    ┌──────┴──┐    ┆    │                │    ┆   │    ┌──────┴──┐    ┆
┆   │    │         │    ┆    └────────────────┘    ┆   │    │         │    ┆
┆   │   String  Integer ┆                          ┆   │   String  Integer ┆
┆   │   Date    Float   ┆                          ┆   │   Date    Float   ┆
┆   │   Time    Boolean ┆                          ┆   │   Time    Boolean ┆
┆   │                   ┆                          ┆   │                   ┆
┆   └──────┐            ┆                          ┆   └──────┐            ┆
┆          │            ┆                          ┆          │            ┆
┆     Contains          ┆                          ┆     Contains          ┆
┆     more Models       ┆                          ┆     more Models       ┆
┆     (recursive)       ┆                          ┆     (recursive)       ┆
┆                       ┆                          ┆                       ┆
╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯                          ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
The Value class, transformation, and serialization formats
╔═══════════════════════╗                          ╔═══════════════════════╗
║LutaML Value Class FOO ║                          ║  Serialization Value  ║
╚═══════════════════════╝                          ╚═══════════════════════╝
╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮                          ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
┆   ┌───────────────┐   ┆                          ┆   ┌───────────────┐   ┆
┆   │     Value     │   ┆   ┌──────────────────┐   ┆   │   XML Value   │   ┆
┆   └───────────────┘   ┆──►│ Value Serializer │──►┆   └───────────────┘   ┆
┆   ┌───────────────┐   ┆   └──────────────────┘   ┆   ┌───────────────┐   ┆
┆   │Primitive Types│   ┆                          ┆   │XML Value Types│   ┆
┆   └───────────────┘   ┆                          ┆   └───────────────┘   ┆
┆ ┌───┘                 ┆                          ┆ ┌───┘                 ┆
┆ ├─ string             ┆                          ┆ ├─ xs:string          ┆
┆ ├─ integer            ┆                          ┆ ├─ xs:integer         ┆
┆ ├─ float              ┆                          ┆ ├─ xs:decimal         ┆
┆ ├─ boolean            ┆                          ┆ ├─ xs:boolean         ┆
┆ ├─ date               ┆                          ┆ ├─ xs:date            ┆
┆ ├─ time_without_date  ┆                          ┆ ├─ xs:time            ┆
┆ ├─ date_time          ┆                          ┆ ├─ xs:dateTime        ┆
┆ ├─ time               ┆                          ┆ ├─ xs:decimal         ┆
┆ ├─ decimal            ┆                          ┆ ├─ xs:anyType         ┆
┆ └─ hash               ┆                          ┆ └─ (complex element)  ┆
╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯                          ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
           │
           ▼
  ┌───────────────────┐
  │ Value Transformer │
  └───────────────────┘
           │
           ▼
╔═══════════════════════╗
║LutaML Value Class BAR ║
╚═══════════════════════╝
╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
┆   ┌───────────────┐   ┆
┆   │     Value     │   ┆
┆   └───────────────┘   ┆
┆   ┌───────────────┐   ┆
┆   │Primitive Types│   ┆
┆   └───────────────┘   ┆
┆ ┌───┘                 ┆
┆ ├─ string             ┆
┆ ├─ integer            ┆
┆ ├─ float              ┆
┆ ├─ boolean            ┆
┆ ├─ date               ┆
┆ ├─ time_without_date  ┆
┆ ├─ date_time          ┆
┆ ├─ time               ┆
┆ ├─ decimal            ┆
┆ └─ hash               ┆
╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
Example 2. Example of LutaML Model instance transformed into a serialization model and serialized to JSON
╔═════════════════════╗     ╔═════════════════════╗     ╔═════════════════════╗
║ Studio (Core Model) ║     ║     JSON Model      ║     ║   Serialized JSON   ║
╚═════════════════════╝     ╚═════════════════════╝     ╚═════════════════════╝

  name: "Studio 1"       ┌─► {                      ┌─► {
  address:               │     "name": "...",       │     "name": "Studio 1",
    ├── street: "..."    │     "address": {         │     "address": {
    └── city: "..."      │       "street": "...",   │       "street": "...",
  kilns:               ──┤       "city": "..."    ──┤       "city": "..."
    ├── count: 3         │     },                   │     },
    └── temp: 1200       │    "kilnsCount": ...,    │     "kilnsCount": 3,
                         │    "kilnsTemp": ...      │     "kilnsTemp": 1200
                         └─► }                      └─► }

Installation

Add this line to your application’s Gemfile:

gem 'lutaml-model'

And then execute:

bundle install

Or install it yourself as:

gem install lutaml-model

Model

General

There are two ways to define an information model in Lutaml::Model:

  • Inheriting from the Lutaml::Model::Serializable class

  • Including the Lutaml::Model::Serialize module

Definition

Through inheritance

The simplest way to define a model is to create a class that inherits from Lutaml::Model::Serializable.

The attribute class method is used to define attributes.

require 'lutaml/model'

class Kiln < Lutaml::Model::Serializable
  attribute :brand, :string
  attribute :capacity, :integer
  attribute :temperature, :integer
end

Through inclusion

If the model class already has a super class that it inherits from, the model can be extended using the Lutaml::Model::Serialize module.

require 'lutaml/model'

class Kiln < SomeSuperClass
  include Lutaml::Model::Serialize

  attribute :brand, :string
  attribute :capacity, :integer
  attribute :temperature, :integer
end

Inheritance

A model can inherit from another model to inherit all attributes and methods of the parent model, allowing for code reusability and a clear model hierarchy.

Syntax:

class Superclass < Lutaml::Model::Serializable
  # attribute ...
  # serialization blocks
end

class Subclass < Superclass
  # attributes are additive
  # serialization blocks are replaced
end

An inherited model has the following characteristics:

  • All attributes are inherited from the parent model.

  • Additional calls to attribute in the child model are additive, unless the attribute name is the same as an attribute in the parent model.

  • Serialization blocks, such as xml and key_value are replaced when defined.

    • In order to selectively import serialization mapping rules from the parent model, the import_model_mappings method can be used (see [import_model_mappings]).

Comparison

A Serialize / Serializable object can be compared with another object of the same class using the == operator. This is implemented through the ComparableModel module.

Two objects are considered equal if they have the same class and all their attributes are equal. This behavior differs from the typical Ruby behavior, where two objects are considered equal only if they have the same object ID.

Note
Two Serialize objects will have the same hash value if they have the same class and all their attributes are equal.
> a = Kiln.new(brand: 'Kiln 1', capacity: 100, temperature: 1050)
> b = Kiln.new(brand: 'Kiln 1', capacity: 100, temperature: 1050)
> a == b
> # true
> a.hash == b.hash
> # true

Value types

General types

Lutaml::Model supports the following attribute value types.

Every type has a corresponding Ruby class and a serialization format type.

Table 1. Mapping between Lutaml::Model::Type classes, Ruby equivalents and serialization format types
Lutaml::Model::Type Ruby class XML JSON YAML Example value

:string

String

xs:string

string

string

"text"

:integer

Integer

xs:integer

number

integer

42

:float

Float

xs:decimal

number

float

3.14

:boolean

TrueClass/FalseClass

xs:boolean

boolean

boolean

true, false

:date

Date

xs:date

string

string

2024-01-01 (JSON/YAML "2024-01-01")

:time_without_date

Time

xs:time

string

string

"12:34:56"

:date_time

DateTime

xs:dateTime

string

string

"2024-01-01T12:00:00+00:00"

:time

Time

xs:dateTime

string

string

"2024-01-01T12:00:00+00:00"

:decimal (optional)

BigDecimal

xs:decimal

number

float

123.45

:hash

Hash

complex element

object

map

{key: "value"}

(nil value)

nil

xs:anyType

null

null

null

Decimal type

Warning
Decimal is an optional feature.

The Decimal type is a value type that is disabled by default.

Note
The reason why the Decimal type is disabled by default is that the BigDecimal class became optional to the standard Ruby library from Ruby 3.4 onwards. The Decimal type is only enabled when the bigdecimal library is loaded.

The following code needs to be run before using (and parsing) the Decimal type:

require 'bigdecimal'

If the bigdecimal library is not loaded, usage of the Decimal type will raise a Lutaml::Model::TypeNotSupportedError.

Custom type

A custom class can be used as an attribute type. The custom class must inherit from Lutaml::Model::Type::Value or a class that inherits from it.

A class inheriting from the Value class carries the attribute value which stores the one-and-only "true" value that is independent of serialization formats.

The minimum requirement for a custom class is to implement the following methods:

self.cast(value)

Assignment of an external value to the Value class to be set as value. Casts the value to the custom type.

self.serialize(value)

Serializes the custom type to an object (e.g. a string). Takes the internal value and converts it into an output suitable for serialization.

Example 3. Using a custom value type to normalize a postcode with minimal methods
class FiveDigitPostCode < Lutaml::Model::Type::String
  def self.cast(value)
    value = value.to_s if value.is_a?(Integer)

    unless value.is_a?(::String)
      raise Lutaml::Model::InvalidValueError, "Invalid value for type 'FiveDigitPostCode'"
    end

    # Pad zeros to the left
    value.rjust(5, '0')
  end

  def self.serialize(value)
    value
  end
end

class Studio < Lutaml::Model::Serializable
  attribute :postcode, FiveDigitPostCode
end

Serialization of custom types

The serialization of custom types can be made to differ per serialization format by defining methods in the class definitions. This requires additional methods than the minimum required for a custom class (i.e. self.cast(value) and self.serialize(value)).

This is useful in the case when different serialization formats of the same model expect differentiated value representations.

The methods that can be overridden are named:

self.from_{format}(serialized_string)

Deserializes a string of the serialization format and returns the object to be assigned to the Value class' value.

to_{format}

Serializes the object to a string of the serialization format.

The {format} part of the method name is the serialization format in lowercase (e.g. hash, json, xml, yaml, toml).

Example 4. Using custom serialization methods to handle a high-precision date-time type

Suppose in XML we handle a high-precision date-time type that requires custom serialization methods, but other formats such as JSON do not support this type.

For instance, in the normal DateTime class, the serialized string is 2012-04-07T01:51:37+02:00, and the high-precision format is 2012-04-07T01:51:37.112+02:00.

We create HighPrecisionDateTime class is a custom class that inherits from Lutaml::Model::Type::DateTime.

class HighPrecisionDateTime < Lutaml::Model::Type::DateTime
  # Inherit the `self.cast(value)` and `self.serialize(value)` methods
  # from Lutaml::Model::Type::DateTime

  # The format looks like this `2012-04-07T01:51:37.112+02:00`
  def self.from_xml(xml_string)
    ::DateTime.parse(xml_string)
  end

  # The %L adds milliseconds to the time
  def to_xml
    value.strftime('%Y-%m-%dT%H:%M:%S.%L%:z')
  end
end

class Ceramic < Lutaml::Model::Serializable
  attribute :kiln_firing_time, HighPrecisionDateTime
  xml do
    root 'ceramic'
    map_element 'kilnFiringTime', to: :kiln_firing_time
    # ...
  end
end

An XML snippet with the high-precision date-time type:

<ceramic>
  <kilnFiringTime>2012-04-07T01:51:37.112+02:00</kilnFiringTime>
  <!-- ... -->
</ceramic>

When loading the XML snippet, the HighPrecisionDateTime class will be used to parse the high-precision date-time string.

However, when serializing to JSON, the value will have the high-precision part lost due to the inability of JSON to handle high-precision date-time.

> c = Ceramic.from_xml(xml)
> #<Ceramic:0x0000000104ac7240 @kiln_firing_time=#<HighPrecisionDateTime:0x0000000104ac7240 @value=2012-04-07 01:51:37.112000000 +0200>>
> c.to_json
> # {"kilnFiringTime":"2012-04-07T01:51:37+02:00"}

Attributes

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

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 6. 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 7. 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 8. 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>
Note
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:

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

  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 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 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, 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 9. 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 is computed dynamically based on an instance method instead of storing a static value. It is defined using the method: option.

Syntax:

attribute :name_of_attribute, method: :instance_method_name
Example 10. Defining methods as attributes
class Invoice < Lutaml::Model::Serializable
  attribute :subtotal, :float
  attribute :tax, :float
  attribute :total, 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.

Note
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 11. 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.

Importable models for reuse

An importable model is a model that can be imported into another model using the import_* directive.

This feature works both with XML and key-value formats.

  • The import order determines how elements and attributes are overwritten.

  • An importable model with XML serialization mappings requires setting the model’s XML serialization configuration with the no_root directive.

The model can be imported into another model using the following directives:

import_model

imports both attributes and mappings.

import_model_attributes

imports only attributes.

import_model_mappings

imports only mappings.

Note
Models with no_root can only be parsed through parent models. Direct calling NoRootModel.from_xml will raise a NoRootMappingError.
Note
Namespaces are not currently supported in importable models. If namespace is defined with no_root, NoRootNamespaceError will be raised.
Example 12. Importing model components using an importable model
class GroupOfItems < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :type, :string
  attribute :code, :string

  xml do
    no_root
    sequence do
      map_element "name", to: :name
      map_element "type", to: :type, namespace: "http://www.example.com", prefix: "ex1"
    end
    map_attribute "code", to: :code
  end
end

class ComplexType < Lutaml::Model::Serializable
  attribute :tag, AttributeValueType
  attribute :content, :string
  attribute :group, :string
  import_model_attributes GroupOfItems

  xml do
    root "GroupOfItems"
    map_attribute "tag", to: :tag
    map_content to: :content
    map_element :group, to: :group
    import_model_mappings GroupOfItems
  end
end

class SimpleType < Lutaml::Model::Serializable
  import_model GroupOfItems
end

class GenericType < Lutaml::Model::Serializable
  import_model_mappings GroupOfItems
end
<GroupOfItems xmlns:ex1="http://www.example.com">
  <name>Name</name>
  <ex1:type>Type</ex1:type>
</GroupOfItems>
> parsed = GroupOfItems.from_xml(xml)
> # Lutaml::Model::NoRootMappingError: "GroupOfItems has `no_root`, it allowed only for reusable models"

Attribute value transform

An attribute value transformation is used when the value of an attribute needs to be transformed around assignment.

There are occasions where the value of an attribute is to be transformed during assignment and retrieval, such that when the external usage of the value differs from the internal model representation.

Note
Value transformation can be applied at the attribute-level or at the serialization-mapping level. They can also be applied together.

Given a model that stores a measurement composed of a numerical value and a unit, where the numerical value is used for calculations inside the model, but the external representation of that value is a string (across all serialization formats).

  • Internal: number: 10.20, unit: cm.

  • External: "10.20 cm"

The transform option at the attribute method is used to define a transformation Proc for the attribute value.

Syntax:

class SomeObject < Lutaml::Model::Serializable
  attribute :attribute_name, {attr_type}, transform: {
    export: ->(value) { ... },
    import: ->(value) { ... }
  }
end

The transform option also support collection attributes.

Where,

attribute_name

The name of the attribute.

attr_type

The type of the attribute.

transform

The option to define a transformation for the attribute value.

export

The transformation Proc for the value when it is being retrieved from the model.

import

The transformation Proc for the value when it is being assigned to the model.

Example 13. Demonstrating attribute-level value transformation procs
class Ceramic < Lutaml::Model::Serializable
  attribute :name, :string, transform: {
    export: ->(value) { value.upcase },
    import: ->(value) { value.downcase }
  }
end
> c = Ceramic.new(name: "Celadon")
> c.name
> # "CELADON"
> c.instance_attribute_get(:@name)
> # "Celadon"
> Ceramic.new(name: "Celadon").name = "Raku"
> # "RAKU"

Value validation

General

There are several mechanisms to validate attribute values in Lutaml::Model.

Values of an enumeration

An attribute can be defined as an enumeration by using the values directive.

The values directive is used to define acceptable values in an attribute. If any other value is given, a Lutaml::Model::InvalidValueError will be raised.

Syntax:

attribute :name_of_attribute, Type, values: [value1, value2, ...]

The values set inside the values: option can be of any type, but they must match the type of the attribute. The values are compared using the == operator, so the type must implement the == method.

Also, If all the elements in values directive are strings then lutaml-model add some enum convenience methods, for each of the value the following three methods are added

  • value1: will return value if set

  • value1?: will return true if value is set, false otherwise

  • value1=: will set the value of name_of_attribute equal to value1 if truthy value is given, and remove it otherwise.

Example 14. Using the values directive to define acceptable values for an attribute (basic types)
class GlazeTechnique < Lutaml::Model::Serializable
  attribute :name, :string, values: ["Celadon", "Raku", "Majolica"]
end
> GlazeTechnique.new(name: "Celadon").name
> # "Celadon"
> GlazeTechnique.new(name: "Raku").name
> # "Raku"
> GlazeTechnique.new(name: "Majolica").name
> # "Majolica"
> GlazeTechnique.new(name: "Earthenware").name
> # Lutaml::Model::InvalidValueError: Invalid value for attribute 'name'

The values can be Serialize objects, which are compared using the == and the hash methods through the Lutaml::Model::ComparableModel module.

Example 15. Using the values directive to define acceptable values for an attribute (Serializable objects)
class Ceramic < Lutaml::Model::Serializable
  attribute :type, :string
  attribute :firing_temperature, :integer
end

class CeramicCollection < Lutaml::Model::Serializable
  attribute :featured_piece,
            Ceramic,
            values: [
              Ceramic.new(type: "Porcelain", firing_temperature: 1300),
              Ceramic.new(type: "Stoneware", firing_temperature: 1200),
              Ceramic.new(type: "Earthenware", firing_temperature: 1000),
            ]
end
> CeramicCollection.new(featured_piece: Ceramic.new(type: "Porcelain", firing_temperature: 1300)).featured_piece
> # Ceramic:0x0000000104ac7240 @type="Porcelain", @firing_temperature=1300
> CeramicCollection.new(featured_piece: Ceramic.new(type: "Bone China", firing_temperature: 1300)).featured_piece
> # Lutaml::Model::InvalidValueError: Invalid value for attribute 'featured_piece'

Serialize provides a validate method that checks if all its attributes have valid values. This is necessary for the case when a value is valid at the component level, but not accepted at the aggregation level.

If a change has been made at the component level (a nested attribute has changed), the aggregation level needs to call the validate method to verify acceptance of the newly updated component.

Example 16. Using the validate method to check if all attributes have valid values
> collection = CeramicCollection.new(featured_piece: Ceramic.new(type: "Porcelain", firing_temperature: 1300))
> collection.featured_piece.firing_temperature = 1400
> # No error raised in changed nested attribute
> collection.validate
> # Lutaml::Model::InvalidValueError: Invalid value for attribute 'featured_piece'

String values restricted to patterns

An attribute that accepts a string value accepts value validation using regular expressions.

Syntax:

attribute :name_of_attribute, :string, pattern: /regex/
Example 17. Using the pattern option to restrict the value of an attribute

In this example, the color attribute takes hex color values such as #ccddee.

A regular expression can be used to validate values assigned to the attribute. In this case, it is /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.

class Glaze < Lutaml::Model::Serializable
  attribute :color, :string, pattern: /\A#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\z/
end
> Glaze.new(color: '#ff0000').color
> # "#ff0000"
> Glaze.new(color: '#ff000').color
> # Lutaml::Model::InvalidValueError: Invalid value for attribute 'color'

Attribute value defaults

Specify default values for attributes using the default option. The default option can be set to a value or a lambda that returns a value.

Syntax:

attribute :name_of_attribute, Type, default: -> { value }
Example 18. Using the default option to set a default value for an attribute
class Glaze < Lutaml::Model::Serializable
  attribute :color, :string, default: -> { 'Clear' }
  attribute :temperature, :integer, default: -> { 1050 }
end
> Glaze.new.color
> # "Clear"
> Glaze.new.temperature
> # 1050

The "default behavior" (pun intended) is to not render a default value if the current value is the same as the default value.

Attribute as raw string

An attribute can be set to read the value as raw string for XML, by using the raw: true option.

Syntax:

attribute :name_of_attribute, :string, raw: true
Example 19. Using the raw option to read raw value for an XML attribute
class Person < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :description, :string, raw: true
end

For the following XML snippet:

<Person>
  <name>John Doe</name>
  <description>
    A <b>fictional person</b> commonly used as a <i>placeholder name</i>.
  </description>
</Person>
> Person.from_xml(xml)
> # <Person:0x0000000107a3ca70
    @description="\n    A <b>fictional person</b> commonly used as a <i>placeholder name</i>.\n  ",
    @element_order=["text", "name", "text", "description", "text"],
    @name="John Doe",
    @ordered=nil>

Serialization model mappings

General

Lutaml::Model allows you to translate a data model into serialization models of various serialization formats including XML, Hash, JSON, YAML, and TOML.

Depending on the serialization format, different methods are supported for defining serialization and deserialization mappings.

Serialization model mappings are defined under the xml, hsh, json, yaml, and toml blocks.

Using the xml, hsh, json, yaml, toml and key_value blocks to define serialization mappings
class Example < Lutaml::Model::Serializable
  xml do
    # ...
  end

  hsh do
    # ...
  end

  json do
    # ...
  end

  yaml do
    # ...
  end

  toml do
    # ...
  end

  key_value do
    # ...
  end
end

XML

Setting root element name

The root method sets the root element tag name of the XML document.

If root is not given, then the snake-cased class name will be used as the root.

Sets the tag name for <example> in XML <example>…​</example>.

Syntax:

xml do
  root 'xml_element_name'
end
Example 20. Setting the root element name to example
class Example < Lutaml::Model::Serializable
  xml do
    root 'example'
  end
end
> Example.new.to_xml
> #<example></example>

Ommiting root element

The root element can be omitted by using the no_root method.

When no_root is used, only map_element can be used because without a root element there cannot be attributes.

Syntax:

xml do
  no_root
end
class NameAndCode < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :code, :string

  xml do
    no_root
    map_element "code", to: :code
    map_element "name", to: :name
  end
end
<name>Name</name>
<code>ID-001</code>
> parsed = NameAndCode.from_xml(xml)
> # <NameAndCode:0x0000000107a3ca70 @code="ID-001", @name="Name">
> parsed.to_xml
> # <code>ID-001</code><name>Name</name>

Mapping all XML content

The map_all tag in XML mapping captures and maps all content within an XML element into a single attribute in the target Ruby object.

The use case for map_all is to tell Lutaml::Model to not parse the content of the XML element at all, and instead handle it as an XML string.

Note
The corresponding method for key-value formats is at Mapping all key-value content.
Warning
Notice that usage of mapping all will lead to incompatibility between serialization formats, i.e. the raw string content will not be portable as objects are across different formats.

This is useful in the case where the content of an XML element is not to be handled by a Lutaml::Model::Serializable object.

This feature is commonly used with custom methods or a custom model object to handle the content.

This includes:

  • nested tags

  • attributes

  • text nodes

The map_all tag is exclusive and cannot be combined with other mappings (map_element, map_content) except for map_attribute for the same element, ensuring it captures the entire inner XML content.

Note
An error is raised if map_all is defined alongside any other mapping in the same XML mapping context.

Syntax:

xml do
  map_all to: :name_of_attribute
end
Example 21. Mapping all the content using map_all
class ExampleMapping < Lutaml::Model::Serializable
  attribute :description, :string

  xml do
    map_all to: :description
  end
end
<ExampleMapping>Content with <b>tags</b> and <i>formatting</i>.</ExampleMapping>
> parsed = ExampleMapping.from_xml(xml)
> puts parsed.all_content
# "Content with <b>tags</b> and <i>formatting</i>."

Mapping elements

The map_element method maps an XML element to a data model attribute.

To handle the <name> tag in <example><name>John Doe</name></example>. The value will be set to John Doe.

Syntax:

xml do
  map_element 'xml_element_name', to: :name_of_attribute
end
Example 22. Mapping the name tag to the name attribute
class Example < Lutaml::Model::Serializable
  attribute :name, :string

  xml do
    root 'example'
    map_element 'name', to: :name
  end
end
<example><name>John Doe</name></example>
> Example.from_xml(xml)
> #<Example:0x0000000104ac7240 @name="John Doe">
> Example.new(name: "John Doe").to_xml
> #<example><name>John Doe</name></example>

If an element is mapped to a model object with the XML root tag name set, the mapped tag name will be used as the root name, overriding the root name.

Example 23. The mapped tag name is used as the root name
class RecordDate < Lutaml::Model::Serializable
  attribute :content, :string

  xml do
    root "recordDate"
    map_content to: :content
  end
end

class OriginInfo < Lutaml::Model::Serializable
  attribute :date_issued, RecordDate, collection: true

  xml do
    root "originInfo"
    map_element "dateIssued", to: :date_issued
  end
end
> RecordDate.new(date: "2021-01-01").to_xml
> #<recordDate>2021-01-01</recordDate>
> OriginInfo.new(date_issued: [RecordDate.new(date: "2021-01-01")]).to_xml
> #<originInfo><dateIssued>2021-01-01</dateIssued></originInfo>

Mapping attributes

The map_attribute method maps an XML attribute to a data model attribute.

Syntax:

xml do
  map_attribute 'xml_attribute_name', to: :name_of_attribute
end
Example 24. Using map_attribute to map the value attribute

The following class will parse the XML snippet below:

class Example < Lutaml::Model::Serializable
  attribute :value, :integer

  xml do
    root 'example'
    map_attribute 'value', to: :value
  end
end
<example value="12"><name>John Doe</name></example>
> Example.from_xml(xml)
> #<Example:0x0000000104ac7240 @value=12>
> Example.new(value: 12).to_xml
> #<example value="12"></example>

The map_attribute method does not inherit the root element’s namespace. To specify a namespace for an attribute, please explicitly declare the namespace and prefix in the map_attribute method.

The following class will parse the XML snippet below:

class Attribute < Lutaml::Model::Serializable
  attribute :value, :integer

  xml do
    root 'example'
    map_attribute 'value', to: :value, namespace: "http://www.tech.co/XMI", prefix: "xl"
  end
end
<example xl:value="20" xmlns:xl="http://www.tech.co/XMI"></example>
> Attribute.from_xml(xml)
> #<Attribute:0x0000000109436db8 @value=20>
> Attribute.new(value: 20).to_xml
> #<example xmlns:xl=\"http://www.tech.co/XMI\" xl:value=\"20\"/>

Mapping content

Content represents the text inside an XML element, inclusive of whitespace.

The map_content method maps an XML element’s content to a data model attribute.

Syntax:

xml do
  map_content to: :name_of_attribute
end
Example 25. Using map_content to map content of the description tag

The following class will parse the XML snippet below:

class Example < Lutaml::Model::Serializable
  attribute :description, :string

  xml do
    root 'example'
    map_content to: :description
  end
end
<example>John Doe is my moniker.</example>
> Example.from_xml(xml)
> #<Example:0x0000000104ac7240 @description="John Doe is my moniker.">
> Example.new(description: "John Doe is my moniker.").to_xml
> #<example>John Doe is my moniker.</example>

CDATA nodes

CDATA is an XML feature that allows the inclusion of text that may contain characters that are unescaped in XML.

While CDATA is not preferred in XML, it is sometimes necessary to handle CDATA nodes for both input and output.

Note
The W3C XML Recommendation explicitly encourages escaping characters over usage of CDATA.

Lutaml::Model supports the handling of CDATA nodes in XML in the following behavior:

  1. When an attribute contains a CDATA node with no text:

    • On reading: The node (CDATA or text) is read as its value.

    • On writing: The value is written as its native type.

  2. When an XML mapping sets cdata: true on map_element or map_content:

    • On reading: The node (CDATA or text) is read as its value.

    • On writing: The value is written as a CDATA node.

  3. When an XML mapping sets cdata: false on map_element or map_content:

    • On reading: The node (CDATA or text) is read as its value.

    • On writing: The value is written as a text node (string).

Syntax:

xml do
  map_content to: :name_of_attribute, cdata: (true | false)
  map_element :name, to: :name, cdata: (true | false)
end
Example 26. Using cdata to map CDATA content

The following class will parse the XML snippet below:

class Example < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :description, :string
  attribute :title, :string
  attribute :note, :string

  xml do
    root 'example'
    map_element :name, to: :name, cdata: true
    map_content to: :description, cdata: true
    map_element :title, to: :title, cdata: false
    map_element :note, to: :note, cdata: false
  end
end
<example><name><![CDATA[John]]></name><![CDATA[here is the description]]><title><![CDATA[Lutaml]]></title><note>Careful</note></example>
> Example.from_xml(xml)
> #<Example:0x0000000104ac7240 @name="John" @description="here is the description" @title="Lutaml" @note="Careful">
> Example.new(name: "John", description: "here is the description", title: "Lutaml", note: "Careful").to_xml
> #<example><name><![CDATA[John]]></name><![CDATA[here is the description]]><title>Lutaml</title><note>Careful</note></example>

Example for mapping

The following class will parse the XML snippet below:

class Ceramic < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :description, :string
  attribute :temperature, :integer

  xml do
    root 'ceramic'
    map_element 'name', to: :name
    map_attribute 'temperature', to: :temperature
    map_content to: :description
  end
end
<ceramic temperature="1200"><name>Porcelain Vase</name> with celadon glaze.</ceramic>
> Ceramic.from_xml(xml)
> #<Ceramic:0x0000000104ac7240 @name="Porcelain Vase", @description=" with celadon glaze.", @temperature=1200>
> Ceramic.new(name: "Porcelain Vase", description: " with celadon glaze.", temperature: 1200).to_xml
> #<ceramic temperature="1200"><name>Porcelain Vase</name> with celadon glaze.</ceramic>

Namespaces

Namespace at root

The namespace method in the xml block sets the namespace for the root element.

Syntax:

Setting default namespace at the root element
xml do
  namespace 'http://example.com/namespace'
end
Setting a prefixed namespace at the root element
xml do
  namespace 'http://example.com/namespace', 'prefix'
end
Example 27. Using the namespace method to set the namespace for the root element
class Ceramic < Lutaml::Model::Serializable
  attribute :type, :string
  attribute :glaze, :string

  xml do
    root 'Ceramic'
    namespace 'http://example.com/ceramic'
    map_element 'Type', to: :type
    map_element 'Glaze', to: :glaze
  end
end
<Ceramic xmlns='http://example.com/ceramic'><Type>Porcelain</Type><Glaze>Clear</Glaze></Ceramic>
> Ceramic.from_xml(xml_file)
> #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze="Clear">
> Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml
> #<Ceramic xmlns="http://example.com/ceramic"><Type>Porcelain</Type><Glaze>Clear</Glaze></Ceramic>
Example 28. Using the namespace method to set a prefixed namespace for the root element
class Ceramic < Lutaml::Model::Serializable
  attribute :type, :string
  attribute :glaze, :string

  xml do
    root 'Ceramic'
    namespace 'http://example.com/ceramic', 'cer'
    map_element 'Type', to: :type
    map_element 'Glaze', to: :glaze
  end
end
<cer:Ceramic xmlns='http://example.com/ceramic'><cer:Type>Porcelain</cer:Type><cer:Glaze>Clear</cer:Glaze></cer:Ceramic>
> Ceramic.from_xml(xml_file)
> #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze="Clear">
> Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml
> #<cer:Ceramic xmlns="http://example.com/ceramic"><cer:Type>Porcelain</cer:Type><cer:Glaze>Clear</cer:Glaze></cer:Ceramic>
Namespace on attribute

If the namespace is defined on a model attribute that already has a namespace, the mapped namespace will be given priority over the one defined in the class.

Syntax:

xml do
  map_element 'xml_element_name', to: :name_of_attribute,
    namespace: 'http://example.com/namespace',
    prefix: 'prefix'
end
namespace

The XML namespace used by this element

prefix

The XML namespace prefix used by this element (optional)

Example 29. Using the namespace option to set the namespace for an element

In this example, glz will be used for Glaze if it is added inside the Ceramic class, and glaze will be used otherwise.

class Ceramic < Lutaml::Model::Serializable
  attribute :type, :string
  attribute :glaze, Glaze

  xml do
    root 'Ceramic'
    namespace 'http://example.com/ceramic'

    map_element 'Type', to: :type
    map_element 'Glaze', to: :glaze, namespace: 'http://example.com/glaze', prefix: "glz"
  end
end

class Glaze < Lutaml::Model::Serializable
  attribute :color, :string
  attribute :temperature, :integer

  xml do
    root 'Glaze'
    namespace 'http://example.com/old_glaze', 'glaze'

    map_element 'color', to: :color
    map_element 'temperature', to: :temperature
  end
end
<Ceramic xmlns='http://example.com/ceramic'>
  <Type>Porcelain</Type>
  <glz:Glaze xmlns='http://example.com/glaze'>
    <color>Clear</color>
    <temperature>1050</temperature>
  </glz:Glaze>
</Ceramic>
> # Using the original Glaze class namespace
> Glaze.new(color: "Clear", temperature: 1050).to_xml
> #<glaze:Glaze xmlns="http://example.com/old_glaze"><color>Clear</color><temperature>1050</temperature></glaze:Glaze>

> # Using the Ceramic class namespace for Glaze
> Ceramic.from_xml(xml_file)
> #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze=#<Glaze:0x0000000104ac7240 @color="Clear", @temperature=1050>>
> Ceramic.new(type: "Porcelain", glaze: Glaze.new(color: "Clear", temperature: 1050)).to_xml
> #<Ceramic xmlns="http://example.com/ceramic"><Type>Porcelain</Type><glz:Glaze xmlns="http://example.com/glaze"><color>Clear</color><temperature>1050</temperature></glz:Glaze></Ceramic>
Namespace with inherit option

The inherit option is used at the element level to inherit the namespace from the root element.

Syntax:

xml do
  map_element 'xml_element_name', to: :name_of_attribute, namespace: :inherit
end
Example 30. Using the inherit option to inherit the namespace from the root element

In this example, the Type element will inherit the namespace from the root.

class Ceramic < Lutaml::Model::Serializable
  attribute :type, :string
  attribute :glaze, :string
  attribute :color, :string

  xml do
    root 'Ceramic'
    namespace 'http://example.com/ceramic', 'cera'
    map_element 'Type', to: :type, namespace: :inherit
    map_element 'Glaze', to: :glaze
    map_attribute 'color', to: :color, namespace: 'http://example.com/color', prefix: 'clr'
  end
end
<cera:Ceramic
  xmlns:cera='http://example.com/ceramic'
  xmlns:clr='http://example.com/color'
  clr:color="navy-blue">
  <cera:Type>Porcelain</cera:Type>
  <Glaze>Clear</Glaze>
</cera:Ceramic>
> Ceramic.from_xml(xml_file)
> #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze="Clear", @color="navy-blue">
> Ceramic.new(type: "Porcelain", glaze: "Clear", color: "navy-blue").to_xml
> #<cera:Ceramic xmlns:cera="http://example.com/ceramic"
  # xmlns:clr='http://example.com/color'
  # clr:color="navy-blue">
  #  <cera:Type>Porcelain</cera:Type>
  #  <Glaze>Clear</Glaze>
  # </cera:Ceramic>

Mixed content

In XML there can be tags that contain content mixed with other tags and where whitespace is significant, such as to represent rich text.

<description><p>My name is <bold>John Doe</bold>, and I'm <i>28</i> years old</p></description>

To map this to Lutaml::Model we can use the mixed option in either way:

  • when defining the model;

  • when referencing the model.

Note
This feature is not supported by Shale.

To specify mixed content, the mixed: true option needs to be set at the xml block’s root method.

Syntax:

xml do
  root 'xml_element_name', mixed: true
end
Example 31. Applying mixed to treat root as mixed content
class Paragraph < Lutaml::Model::Serializable
  attribute :bold, :string, collection: true # allows multiple bold tags
  attribute :italic, :string

  xml do
    root 'p', mixed: true

    map_element 'bold', to: :bold
    map_element 'i', to: :italic
  end
end
> Paragraph.from_xml("<p>My name is <bold>John Doe</bold>, and I'm <i>28</i> years old</p>")
> #<Paragraph:0x0000000104ac7240 @bold="John Doe", @italic="28">
> Paragraph.new(bold: "John Doe", italic: "28").to_xml
> #<p>My name is <bold>John Doe</bold>, and I'm <i>28</i> years old</p>

Ordered content

ordered: true maintains the order of XML Elements, while mixed: true preserves the order of XML Elements and Content.

Note
When both options are used, mixed: true takes precedence.

To specify ordered content, the ordered: true option needs to be set at the xml block’s root method.

Syntax:

xml do
  root 'xml_element_name', ordered: true
end
Example 32. Applying ordered to treat root as ordered content
class RootOrderedContent < Lutaml::Model::Serializable
  attribute :bold, :string
  attribute :italic, :string
  attribute :underline, :string

  xml do
    root "RootOrderedContent", ordered: true
    map_element :bold, to: :bold
    map_element :italic, to: :italic
    map_element :underline, to: :underline
  end
end
<RootOrderedContent>
  <underline>Moon</underline>
  <italic>384,400 km</italic>
  <bold>bell</bold>
</RootOrderedContent>
> instance = RootOrderedContent.from_xml(xml)
> #<RootOrderedContent:0x0000000104ac7240 @bold="bell", @italic="384,400 km", @underline="Moon">
> instance.to_xml
> #<RootOrderedContent><underline>Moon</underline><italic>384,400 km</italic><bold>bell</bold></RootOrderedContent>

Without Ordered True:

class RootOrderedContent < Lutaml::Model::Serializable
  attribute :bold, :string
  attribute :italic, :string
  attribute :underline, :string

  xml do
    root "RootOrderedContent"
    map_element :bold, to: :bold
    map_element :italic, to: :italic
    map_element :underline, to: :underline
  end
end
<RootOrderedContent>
  <underline>Moon</underline>
  <italic>384,400 km</italic>
  <bold>bell</bold>
</RootOrderedContent>
> instance = RootOrderedContent.from_xml(xml)
> #<RootOrderedContent:0x0000000104ac7240 @bold="bell", @italic="384,400 km", @underline="Moon">
> instance.to_xml
> #<RootOrderedContent>\n  <bold>bell</bold>\n  <italic>384,400 km</italic>\n  <underline>Moon</underline>\n</RootOrderedContent>

Sequence

The sequence directive specifies that the defined attributes must appear in a specified order in XML.

Note
Sequence only supports map_element mappings.

Syntax:

xml do
  sequence do
    map_element 'xml_element_name_1', to: :name_of_attribute_1
    map_element 'xml_element_name_2', to: :name_of_attribute_2
    # Add more map_element lines as needed to establish a complete sequence
  end
end

The appearance of the elements in the XML document must match the order defined in the sequence block. In this case, the <xml_element_name_1> element should appear before the <xml_element_name_2> element.

Example 33. Using the sequence keyword to define a set of elements in desired order.
class Kiln < Lutaml::Model::Serializable
  attribute :id, :string
  attribute :name, :string
  attribute :type, :string
  attribute :color, :string

  xml do
    sequence do
      map_element :id, to: :id
      map_element :name, to: :name
      map_element :type, to: :type
      map_element :color, to: :color
    end
  end
end

class KilnCollection < Lutaml::Model::Serializable
  attribute :kiln, Kiln, collection: 1..2

  xml do
    root "collection"
    map_element "kiln", to: :kiln
  end
end
<collection>
  <kiln>
    <id>1</id>
    <name>Nick</name>
    <type>Hard</type>
    <color>Black</color>
  </kiln>
  <kiln>
    <id>2</id>
    <name>John</name>
    <type>Soft</type>
    <color>White</color>
  </kiln>
</collection>
> parsed = Kiln.from_xml(xml)
# => [
#<Kiln:0x0000000104ac7240 @id="1", @name="Nick", @type="Hard", @color="Black">,
#<Kiln:0x0000000104ac7240 @id="2", @name="John", @type="Soft", @color="White">
#]

> bad_xml = <<~HERE
<collection>
  <kiln>
    <name>Nick</name>
    <id>1</id>
    <color>Black</color>
    <type>Hard</type>
  </kiln>
</collection>
HERE
> parsed = Kiln.from_xml(bad_xml)
# => Lutaml::Model::ValidationError: Element 'name' is out of order in 'kiln' element

Automatic support of xsi:schemaLocation

The W3C "XMLSchema-instance" namespace describes a number of attributes that can be used to control the behavior of XML processors. One of these attributes is xsi:schemaLocation.

The xsi:schemaLocation attribute locates schemas for elements and attributes that are in a specified namespace. Its value consists of pairs of a namespace URI followed by a relative or absolute URL where the schema for that namespace can be found.

Usage of xsi:schemaLocation in an XML element depends on the declaration of the XML namespace of xsi, i.e. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance". Without this namespace LutaML will not be able to serialize the xsi:schemaLocation attribute.

Note
It is most commonly attached to the root element but can appear further down the tree.

The following snippet shows how xsi:schemaLocation is used in an XML document:

<cera:Ceramic
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:cera="http://example.com/ceramic"
  xmlns:clr='http://example.com/color'
  xsi:schemaLocation=
    "http://example.com/ceramic http://example.com/ceramic.xsd
     http://example.com/color http://example.com/color.xsd"
  clr:color="navy-blue">
  <cera:Type>Porcelain</cera:Type>
  <Glaze>Clear</Glaze>
</cera:Ceramic>

LutaML::Model supports the xsi:schemaLocation attribute in all XML serializations by default, through the schema_location attribute on the model instance object.

Example 34. Retrieving and setting the xsi:schemaLocation attribute in XML serialization

In this example, the xsi:schemaLocation attribute will be automatically supplied without the explicit need to define in the model, and allows for round-trip serialization.

class Ceramic < Lutaml::Model::Serializable
  attribute :type, :string
  attribute :glaze, :string
  attribute :color, :string

  xml do
    root 'Ceramic'
    namespace 'http://example.com/ceramic', 'cera'
    map_element 'Type', to: :type, namespace: :inherit
    map_element 'Glaze', to: :glaze
    map_attribute 'color', to: :color, namespace: 'http://example.com/color', prefix: 'clr'
  end
end

xml_content = <<~HERE
<cera:Ceramic
  xmlns:cera="http://example.com/ceramic"
  xmlns:clr="http://example.com/color"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  clr:color="navy-blue"
  xsi:schemaLocation="
    http://example.com/ceramic http://example.com/ceramic.xsd
    http://example.com/color http://example.com/color.xsd
  ">
  <cera:Type>Porcelain</cera:Type>
  <Glaze>Clear</Glaze>
</cera:Ceramic>
HERE
> c = Ceramic.from_xml(xml_content)
=>
#<Ceramic:0x00000001222bdd60
...
> schema_loc = c.schema_location
#<Lutaml::Model::SchemaLocation:0x0000000122773760
...
> schema_loc
=>
#<Lutaml::Model::SchemaLocation:0x0000000122773760
 @namespace="http://www.w3.org/2001/XMLSchema-instance",
 @original_schema_location="http://example.com/ceramic http://example.com/ceramic.xsd http://example.com/color http://example.com/color.xsd",
 @prefix="xsi",
 @schema_location=
  [#<Lutaml::Model::Location:0x00000001222bd018 @location="http://example.com/ceramic.xsd", @namespace="http://example.com/ceramic">,
   #<Lutaml::Model::Location:0x00000001222bcfc8 @location="http://example.com/color.xsd", @namespace="http://example.com/color">]>
> new_c = Ceramic.new(type: "Porcelain", glaze: "Clear", color: "navy-blue", schema_location: schema_loc).to_xml
> puts new_c
# <cera:Ceramic
#   xmlns:cera="http://example.com/ceramic"
#   xmlns:clr="http://example.com/color"
#   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
#   clr:color="navy-blue"
#   xsi:schemaLocation="
#     http://example.com/ceramic http://example.com/ceramic.xsd
#     http://example.com/color http://example.com/color.xsd
#   ">
#   <cera:Type>Porcelain</cera:Type>
#   <cera:Glaze>Clear</cera:Glaze>
# </cera:Ceramic>
Note
For details on xsi:schemaLocation, please refer to the W3C XML standard.

Character encoding

General

Lutaml::Model XML adapters use a default encoding of UTF-8 for both input and output.

Serialization data to be parsed (deserialization) and serialization data to be exported (serialization) may be in a different character encoding than the default encoding used by the Lutaml::Model XML adapter. This mismatch may lead to incorrect data reading or incompatibilities when exporting data.

The possible values for setting character encoding to are:

  • A valid encoding value, e.g. UTF-8, Shift_JIS, ASCII;

  • nil to use the default encoding of the adapter. The behavior differs based on the adapter used.

    • Nokogiri: UTF-8. The encoding is set to the default encoding of the Nokogiri library, which is UTF-8.

    • Oga: UTF-8. The encoding is set to the default encoding of the Oga library, which uses UTF-8.

    • Ox: ASCII-8bit. The encoding is set to the default encoding of the Ox library, which uses ASCII-8bit.

When the encoding option is not set, the default encoding of UTF-8 is used.

Serialization character encoding (exporting)
General

There are two ways to set the character encoding of the XML document during serialization:

Instance setting

Setting the instance-level encoding option by setting ModelClassInstance.encoding('…​'). This setting only affects serialization.

Per-export setting

Setting the encoding option when calling for serialization action using the ModelClassInstance.to_xml(…​, encoding: …​) method.

Instance setting

The encoding value of an instance sets the character encoding of the XML document during serialization.

Syntax:

ModelClassInstance.encoding = {encoding_value}

Where,

ModelClassInstance

An instance of the class that inherits from Lutaml::Model::Serializable.

{encoding_value}

The encoding of the output data.

Example 35. Character encoding set to instance is reflected in its serialization output
class JapaneseCeramic < Lutaml::Model::Serializable
  attribute :glaze_type, :string
  attribute :description, :string

  xml do
    root 'JapaneseCeramic'
    map_attribute 'glazeType', to: :glaze_type
    map_element 'description' to: :description
  end
end
# Create a new instance with UTF-8 data
> instance = JapaneseCeramic.new(glaze_type: "志野釉", description: "東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)")
#=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="志野釉", @description="東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)">

# Set character encoding to Shift_JIS
> instance.encoding = "Shift_JIS"
#=> "Shift_JIS"

# Serialize the instance
> serialization_output = instance.to_xml
#=> #<JapaneseCeramic><glazeType>\x{5FD8}\x{91CE}\x{91C9}</glazeType><description>\x{6771}\x{4EAC}\x{56FD}\x{7ACB}\x{535A}\x{7269}\x{9928}\x{30B3}\x{30EC}\x{30AF}\x{30B7}\x{30E7}\x{30F3}\x{306E}\x{7BC0}\x{8336}\x{7897}\x{300C}\x{6A4B}\x{672C}\x{300D}\x{FF08}\x{6853}\x{5C71}\x{6642}\x{4EE3}\x{FF09}</description></JapaneseCeramic>

# Check character encoding of output
> serialization_output.encoding
#=> "Shift_JIS"
Per-export setting

The encoding option is used in the ModelClass#to_xml(…​, encoding: …​) call to set the character encoding of the XML document during serialization.

The per-export encoding setting supersedes the instance-level encoding setting.

Syntax:

ModelClassInstance.to_xml(encoding: {encoding_value})

Where,

ModelClassInstance

An instance of the class that inherits from Lutaml::Model::Serializable.

{encoding_value}

The encoding of the output data.

The following class will parse the XML snippet below:

class Ceramic < Lutaml::Model::Serializable
  attribute :potter, :string
  attribute :description, :string
  attribute :temperature, :integer

  xml do
    root 'ceramic'
    map_element 'potter', to: :potter
    map_content to: :description
  end
end
<ceramic><potter>John &#x0026; Jane</potter> A &#x2211; series of &#x220F; porcelain &#xB5; vases.</ceramic>
# Object with attributes
> ceramic_instance = Ceramic.new(potter: "John & Jane", description: " A ∑ series of ∏ porcelain µ vases.")
> #<Ceramic:0x0000000104ac7240 @potter="John & Jane", @description=" A ∑ series of ∏ porcelain µ vases.">

# Parsing the XML snippet with the default encoding of UTF-8
> ceramic_parsed = Ceramic.from_xml(xml)
> #<Ceramic:0x0000000104ac7242 @potter="John & Jane", @description=" A ∑ series of ∏ porcelain µ vases.">

# Object with attributes is equal to the parsed object
> ceramic_parsed == ceramic_instance
> # true

# Using the default encoding of UTF-8
> ceramic_instance.to_xml
> #<ceramic><potter>John &amp; Jane</potter> A ∑ series of ∏ porcelain µ vases.</ceramic>

# Using the default encoding of the adapter, which is UTF-8 in this case
> ceramic_instance.to_xml(encoding: nil)
> #<ceramic><potter>John &amp; Jane</potter> A &#x2211; series of &#x220F; porcelain &#xB5; vases.</ceramic>

# Using ASCII encoding
> ceramic_instance.to_xml(encoding: "ASCII")
> #<ceramic><potter>John &amp; Jane</potter> A &#8721; series of &#8719; porcelain &#181; vases.</ceramic>
Example 36. Character encoding set at to_xml overrides instance encoding
class JapaneseCeramic < Lutaml::Model::Serializable
  attribute :glaze_type, :string
  attribute :description, :string

  xml do
    root 'JapaneseCeramic'
    map_attribute 'glazeType', to: :glaze_type
    map_element 'description' to: :description
  end
end
# Create a new instance with UTF-8 data
> instance = JapaneseCeramic.new(glaze_type: "志野釉", description: "東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)")
#=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="志野釉", @description="東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)">

# Set character encoding to Shift_JIS
> instance.encoding = "Shift_JIS"
#=> "Shift_JIS"

# Serialize the instance
> serialization_output = instance.to_xml(encoding: "UTF-8")
#=> #<JapaneseCeramic><glazeType>志野釉</glazeType><description>東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)</description></JapaneseCeramic>

# Check character encoding of output
> serialization_output.encoding
#=> "UTF-8"
Deserialization character encoding (parsing)

The character encoding of the XML document being parsed is specified using the encoding option when the ModelClass.from_{format}(…​) is called.

Syntax:

ModelClass.from_{format}(string_in_format, encoding: {encoding_value})

Where,

ModelClass

The class that inherits from Lutaml::Model::Serializable.

{format}

The format of the input data, e.g. xml, json, yaml, toml.

string_in_format

The input data in the specified format.

{encoding_value}

The encoding of the input data.

Example 37. Setting the encoding option during parsing data not encoded in the default encoding (UTF-8)

Using the definition of JapaneseCeramic at Instance setting.

This XML snippet is in Shift-JIS.

<JapaneseCeramic>
  <glazeType>\x{5FD8}\x{91CE}\x{91C9}</glazeType>
  <description>\x{6771}\x{4EAC}\x{56FD}\x{7ACB}\x{535A}\x{7269}\x{9928}\x{30B3}\x{30EC}\x{30AF}\x{30B7}\x{30E7}\x{30F3}\x{306E}\x{7BC0}\x{8336}\x{7897}\x{300C}\x{6A4B}\x{672C}\x{300D}\x{FF08}\x{6853}\x{5C71}\x{6642}\x{4EE3}\x{FF09}</description>
</JapaneseCeramic>
# Parse the XML snippet with the encoding of Shift_JIS
> instance = JapaneseCeramic.from_xml(xml, encoding: "Shift_JIS")
#=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="志野釉", @description="東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)">

# Check character encoding of the instance
> instance.encoding
#=> "Shift_JIS"

# Serialize the instance using UTF-8
> serialization_output = instance.to_xml(encoding: "UTF-8")
#=> #<JapaneseCeramic><glazeType>志野釉</glazeType><description>東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)</description></JapaneseCeramic>
> serialization_output.encoding
#=> "UTF-8"
Example 38. When the encoding option is not set, the default encoding of the adapter is used

Using the definition of JapaneseCeramic at Instance setting.

This XML snippet is in UTF-8.

<JapaneseCeramic>
  <glazeType>志野釉</glazeType>
  <description>東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)</description>
</JapaneseCeramic>

In adapters that use a default encoding of UTF-8, the content is parsed properly.

> instance = JapaneseCeramic.from_xml(xml, encoding: nil)
#=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="志野釉", @description="東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)">
> instance.encoding
#=> "UTF-8"
> serialization_output = instance.to_xml
#=> #<JapaneseCeramic><glazeType>志野釉</glazeType><description>東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)</description></JapaneseCeramic>
> serialization_output.encoding
#=> "UTF-8"

In adapters that use a default encoding of ASCII-8bit, the content becomes malformed.

> instance = JapaneseCeramic.from_xml(xml, encoding: nil)
#=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="�菑�", @description="�東京国立博物館コレクションの篠茶碗�橋本�桃山時代�">
> instance.encoding
#=> "ASCII-8bit"
> serialization_output = instance.to_xml
#=> #<JapaneseCeramic><glazeType>�菑�</glazeType><description>�東京国立博物館コレクションの篠茶碗�橋本�桃山時代�</description></JapaneseCeramic>
> serialization_output.encoding
#=> "ASCII-8bit"
Example 39. Using an invalid encoding to deserialize causes data corruption

Using the definition of JapaneseCeramic at Instance setting.

This XML snippet is in UTF-8.

<JapaneseCeramic>
  <glazeType>志野釉</glazeType>
  <description>東京国立博物館コレクションの篠茶碗「橋本」(桃山時代)</description>
</JapaneseCeramic>
> JapaneseCeramic.from_xml(xml, encoding: "Shift_JIS")
#=> #<JapaneseCeramic:0x0000000104ac7240 @glaze_type="�菑���p���P", @description="�東京国立博物館コレクションの篠茶碗�橋本�桃山時代�">

Key value data models

General

Key-value data models like Hash, JSON, YAML, and TOML all share a similar structure where data is stored as key-value pairs.

Lutaml::Model works with these formats in a similar way.

Mapping

The map method is used to define key-value mappings.

Syntax:

hsh | json | yaml | toml | key_value do
  map 'key_value_model_attribute_name', to: :name_of_attribute
end

Unified mapping

The key_value method is a streamlined way to map all attributes for serialization into key-value formats including Hash, JSON, YAML, and TOML.

If there is no definite differentiation between the key value formats, the key_value method simplifies defining mappings and improves code readability.

Example 40. Using the map method to define the same mappings across all key-value formats

This example shows how to define a key-value data model with the key_value method which maps the same attributes across all key-value formats.

class CeramicModel < Lutaml::Model::Serializable
  attribute :color, :string
  attribute :glaze, :string
  attribute :description, :string

  key_value do
    map :color, to: color
    map :glz, to: :glaze
    map :desc, to: :description
  end

  # Equivalent to the Hash, JSON, YAML, and TOML mappings.
  #
  # hsh and json and yaml and toml do
  #   map :id, to: color
  #   map :name, to: :full_name
  #   map :status, to: :current_status
  # end
end
{
  "color": "Navy Blue",
  "glz": "Clear",
  "desc": "A ceramic with a navy blue color and clear glaze."
}
color: Navy Blue
glz: Clear
desc: A ceramic with a navy blue color and clear glaze.
> CeramicModel.from_json(json)
> #<CeramicModel:0x0000000104ac7240 @color="Navy Blue", @glaze="Clear", @description="A ceramic with a navy blue color and clear glaze.">
> CeramicModel.new(color: "Navy Blue", glaze: "Clear", description: "A ceramic with a navy blue color and clear glaze.").to_json
> #{"color"=>"Navy Blue", "glz"=>"Clear", "desc"=>"A ceramic with a navy blue color and clear glaze."}

Specific format mappings

Specific key value formats can be mapping independently of other formats, including:

  • hsh for the Hash format

  • json for the JSON format

  • yaml for the YAML format

  • toml for the TOML format

Example 41. Using the map method to define key-value mappings per format
class Example < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :value, :integer

  hsh do
    map 'name', to: :name
    map 'value', to: :value
  end

  json do
    map 'name', to: :name
    map 'value', to: :value
  end

  yaml do
    map 'name', to: :name
    map 'value', to: :value
  end

  toml do
    map 'name', to: :name
    map 'value', to: :value
  end
end
{
  "name": "John Doe",
  "value": 28
}
> Example.from_json(json)
> #<Example:0x0000000104ac7240 @name="John Doe", @value=28>
> Example.new(name: "John Doe", value: 28).to_json
> #{"name"=>"John Doe", "value"=>28}

Mapping all key-value content

The map_all tag captures and maps all content within a serialization format into a single attribute in the target Ruby object.

The use case for map_all is to tell Lutaml::Model to not parse the content at all, and instead handle it as a raw string.

Note
The corresponding method for XML is at Mapping all XML content.
Warning
Notice that usage of mapping all will lead to incompatibility between serialization formats, i.e. the raw string content will not be portable as objects are across different formats.

This is useful when the content needs to be handled as-is without parsing into individual attributes.

The map_all tag is exclusive and cannot be combined with other mappings, ensuring it captures the entire content.

Note
An error is raised if map_all is defined alongside any other mapping in the same mapping context.

Syntax:

hsh | json | yaml | toml | key_value do
  map_all to: :name_of_attribute
end
Example 42. Using map_all to capture all content across different formats
class Document < Lutaml::Model::Serializable
  attribute :content, :string

  hsh do
    map_all to: :content
  end

  json do
    map_all to: :content
  end

  yaml do
    map_all to: :content
  end

  toml do
    map_all to: :content
  end
end

For JSON:

{
  "sections": [
    { "title": "Introduction", "text": "Chapter 1" },
    { "title": "Conclusion", "text": "Final chapter" }
  ],
  "metadata": {
    "author": "John Doe",
    "date": "2024-01-15"
  }
}

For YAML:

sections:
  - title: Introduction
    text: Chapter 1
  - title: Conclusion
    text: Final chapter
metadata:
  author: John Doe
  date: 2024-01-15

The content is preserved exactly as provided:

> doc = Document.from_json(json_content)
> puts doc.content
> # "{\"sections\":[{\"title\":\"Introduction\",\"text\":\"Chapter 1\"},{\"title\":\"Conclusion\",\"text\":\"Final chapter\"}],\"metadata\":{\"author\":\"John Doe\",\"date\":\"2024-01-15\"}}"

> doc = Document.from_yaml(yaml_content)
> puts doc.content
> # "sections:\n  - title: Introduction\n    text: Chapter 1\n  - title: Conclusion\n    text: Final chapter\nmetadata:\n  author: John Doe\n  date: 2024-01-15\n"

Nested attribute mappings

The map method can also be used to map nested key-value data models by referring to a Lutaml::Model class as an attribute class.

class Glaze < Lutaml::Model::Serializable
  attribute :color, :string
  attribute :temperature, :integer

  json do
    map 'color', to: :color
    map 'temperature', to: :temperature
  end
end

class Ceramic < Lutaml::Model::Serializable
  attribute :type, :string
  attribute :glaze, Glaze

  json do
    map 'type', to: :type
    map 'glaze', to: :glaze
  end
end
{
  "type": "Porcelain",
  "glaze": {
    "color": "Clear",
    "temperature": 1050
  }
}
> Ceramic.from_json(json)
> #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze=#<Glaze:0x0000000104ac7240 @color="Clear", @temperature=1050>>
> Ceramic.new(type: "Porcelain", glaze: Glaze.new(color: "Clear", temperature: 1050)).to_json
> #{"type"=>"Porcelain", "glaze"=>{"color"=>"Clear", "temperature"=>1050}}

Collection with keyed elements (keyed collection)

General
Note
This feature is for key-value data model serialization and deserialization only.

The map method with the root_mappings option is used for key-value data that is keyed using an attribute value.

In other words, the key of a key-value pair in a collection is actually the value of an attribute that belongs to the value.

Simply put, the following two data structures are considered to have the same data:

A YAML collection as a keyed object, each key with value of the id attribute
---
vase1:
  name: Imperial Vase
bowl2:
  name: 18th Century Bowl
A YAML collection as an array, the id attribute value located inside each element
---
- id: vase1
  name: Imperial Vase
- id: bowl2
  name: 18th Century Bowl

There are key difference between these two data structures:

  • The keyed object (first data structure) ensures uniqueness of the id attribute value across the collection, while the array (second data structure) does not.

  • The value of the id attribute in the first data structure exists outside of the formal structure of the data object, instead, it only exists at the collection level. On the other hand, the value exists inside the structure of the data object in the second data structure.

The map method with the root_mappings option, in practice, parses the first data structure in the same way that you would access / manipulate the second data structure, while retaining the serialization semantics of using an attribute as key.

As a result, usage of lutaml-model across both types of collections are identical (except when serialized).

Syntax:

class SomeKeyedCollection < Lutaml::Model::Serializable
  attribute :name_of_attribute, AttributeValueType, collection: true

  hsh | json | yaml | toml | key_value do
    map to: :name_of_attribute, (1)
      root_mappings: { (2)
        # `:key` is a reserved keyword
        value_type_attribute_name_for_key: :key, (3)
        # `:value` is a reserved keyword (and optional)
        value_type_attribute_name_for_value: :value, (4)
        # `[path name]` represents the path to access the value in the
        # serialization data model to be assigned to
        # `AttributeValueType.value_type_attribute_name_for_custom_type`
        value_type_attribute_name_for_custom_type: [path name] (5)
      }
  end
end

class AttributeValueType < Lutaml::Model::Serializable
  attribute :value_type_attribute_name_for_key, :string
  attribute :value_type_attribute_name_for_value, :string
  attribute :value_type_attribute_name_for_custom_type, CustomType
end
  1. The map option indicates that this class represents the root of the serialization object being passed in. The name_of_attribute is the name of the attribute that will hold the collection data. (Mandatory)

  2. The root_mappings keyword specifies what the collection key represents and and value for model. (Mandatory)

  3. The key keyword specifies the attribute name of the individual collection object type that represents its key used in the collection. (Mandatory)

  4. The value keyword specifies the attribute name of the individual collection object type that represents its data used in the collection. (Optional, if not specified, the entire object is used as the value.)

  5. The value_type_attribute_name_for_custom_type is the name of the attribute inside the individual collection object (AttributeValueType) that will hold the value accessible in the serialization data model fetched at [path name].

The mapping syntax here is similar to that of Attribute extraction except that the :key and :value keywords are allowed in addition to {path}.

There are 3 cases when working with a keyed collection:

  1. Case 1: Only move the "key" into the collection object.

  2. Case 2: Move the "key" into the collection object, override all other mappings. Maps :key and another attribute, then we override all the other mappings (clean slate)

  3. Case 3: Move the "key" into the collection object to an attribute, map the entire "value" to another attribute of the collection object.

Case 1: Only move the "key" into the collection object

In this case, the "key" of the keyed collection is moved into the collection object, and all other mappings are left as they are.

When the "key" is moved into the collection object, the following happens:

  • The "key" of the keyed collection maps to a particular attribute of the collection’s instance object.

  • The "value" of the keyed collection (with its various content) maps to the collection’s instance object following the collection’s instance object type’s default mappings.

The root_mappings option should only contain one mapping, and the mapping must lead to the :key keyword.

Syntax:

class SomeKeyedCollection < Lutaml::Model::Serializable
  attribute :name_of_attribute, AttributeValueType, collection: true

  hsh | json | yaml | toml | key_value do
    map to: :name_of_attribute,
      root_mappings: {
        value_type_attribute_name_for_key: :key, (1)
      }
  end
end

class AttributeValueType < Lutaml::Model::Serializable
  attribute :value_type_attribute_name_for_key, :string
  attribute :value_type_attribute_name_for_value, :string
  attribute :value_type_attribute_name_for_custom_type, CustomType
end
  1. The :key keyword specifies that the "key" of the keyed collection maps to the value_type_attribute_name_for_key attribute of the collection’s instance object (i.e. AttributeValueType).

Example 43. Using map with root_mappings (only key) to map a keyed collection into individual models

Given this data:

---
vase1:
  name: Imperial Vase
bowl2:
  name: 18th Century Bowl

A model can be defined for this YAML as follows:

# This is a normal Lutaml::Model class
class Ceramic < Lutaml::Model::Serializable
  attribute :ceramic_id, :string
  attribute :ceramic_name, :string

  key_value do
    map 'id', to: :ceramic_id
    map 'name', to: :ceramic_name
  end
end

# This is Lutaml::Model class that represents the collection of Ceramic objects
class CeramicCollection < Lutaml::Model::Serializable
  attribute :ceramics, Ceramic, collection: true

  key_value do
    map to: :ceramics, # All data goes to the `ceramics` attribute
      root_mappings: {
        # The key of an object in this collection is mapped to the ceramic_id
        # attribute of the Ceramic object.
        ceramic_id: :key # "key" is a reserved keyword
      }
  end
end
# Parsing the YAML collection with dynamic data keys
> ceramic_collection = CeramicCollection.from_yaml(yaml)
> #<CeramicCollection:0x0000000104ac7240
  @ceramics=
    [#<Ceramic:0x0000000104ac6e30 @ceramic_id="vase1", @ceramic_name="Imperial Vase">,
     #<Ceramic:0x0000000104ac58f0 @ceramic_id="bowl2", @ceramic_name="18th Century Bowl">]

# NOTE: When an individual Ceramic object is serialized, the `id` attribute is
# the original key in the incoming YAML data, and because there were no mappings defined along with the `:key`, everyting is mapped to the `Ceramic` object using the mappings defined in the `Ceramic` class.
> first_ceramic = ceramic_collection.ceramics.first
> puts first_ceramic.to_yaml
=>
# ---
# id: vase1
# name: Imperial Vase

# NOTE: When in a collection, the `ceramic_id` attribute is used to key the data,
# and it disappears from the individual object.
> puts ceramic_collection.to_yaml
=>
# ---
# vase1:
#   name: Imperial Vase
# bowl2:
#   name: 18th Century Bowl

# NOTE: When the collection is serialized, the `ceramic_id` attribute is used to
# key the data. This is defined through the `map` with `root_mappings` method in
# CeramicCollection.
> new_collection = CeramicCollection.new(ceramics: [
    Ceramic.new(ceramic_id: "vase1", ceramic_name: "Imperial Vase"),
    Ceramic.new(ceramic_id: "bowl2", ceramic_name: "18th Century Bowl")
  ])
> puts new_collection.to_yaml
=>
# ---
# vase1:
#   name: Imperial Vase
# bowl2:
#   name: 18th Century Bowl
Case 2: Mapping the key and complex values

In this use case, the "key" of the keyed collection is moved into the collection object, and all other mappings are overridden.

When more than one mapping rule exists in the root_mappings option, the root_mappings option will override all other mappings in the collection object.

When the "key" is moved into the collection object, the following happens:

  • The "key" of the keyed collection maps to a particular attribute of the collection’s instance object.

  • The data of the "value" of the keyed collection have their own mappings overridden by the new mapping rules of the root_mappings option.

The root_mappings option can contain more than one mapping, with one of the mapping rules leading to the :key keyword.

Syntax:

class SomeKeyedCollection < Lutaml::Model::Serializable
  attribute :name_of_attribute, AttributeValueType, collection: true

  hsh | json | yaml | toml | key_value do
    map to: :name_of_attribute,
      root_mappings: {
        value_type_attribute_name_for_key: :key, (1)
        value_type_attribute_name_for_value_data_1: "serialization_format_name_1", (2)
        value_type_attribute_name_for_value_data_2: "serialization_format_name_2",
        value_type_attribute_name_for_value_data_3: ["path name", ...] (3)
        # ...
      }
  end
end

class AttributeValueType < Lutaml::Model::Serializable
  attribute :value_type_attribute_name_for_key, :string
  attribute :value_type_attribute_name_for_value_data_1, :string
  attribute :value_type_attribute_name_for_value_data_2, SomeType
  attribute :value_type_attribute_name_for_value_data_3, MoreType
  # ...
end
  1. The :key keyword specifies that the "key" of the keyed collection maps to the value_type_attribute_name_for_key attribute of the collection’s instance object (i.e. AttributeValueType).

  2. The serialization_format_name_1 target specifies that the serialization_format_name_2 key of the keyed collection value maps to the value_type_attribute_name_for_value_data_1 attribute of the collection’s instance object.

  3. The [path name] target specifies to fetch from [path name] in the serialization data model to be assigned to the value_type_attribute_name_for_value_data_3 attribute of the collection’s instance object.

When the root_mappings mapping contains more than one mapping rule that is not to :key or :value, the root_mappings mapping will override all other mappings in the collection object. This means that unmapped attributes in root_mappings will not be incorporated in the collection instance objects.

Example 44. Using map with root_mappings (key and complex value) to map a keyed collection into individual models
"vase1":
  type: "vase"
  details:
    name: "Imperial Vase"
    insignia: "Tang Tianbao"
  urn:
    primary: "urn:ceramic:vase:vase1"
"bowl2":
  type: "bowl"
  details:
    name: "18th Century Bowl"
    insignia: "Ming Wanli"
  urn:
    primary: "urn:ceramic:bowl:bowl2"

A model can be defined for this YAML as follows:

# This is a normal Lutaml::Model class
class CeramicDetails < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :insignia, :string

  key_value do
    map 'name', to: :name
    map 'insignia', to: :insignia
  end
end

# This is a normal Lutaml::Model class
class Ceramic < Lutaml::Model::Serializable
  attribute :ceramic_id, :string
  attribute :ceramic_type, :string
  attribute :ceramic_details, CeramicDetails
  attribute :ceramic_urn, :string

  key_value do
    map 'id', to: :ceramic_id
    map 'type', to: :ceramic_type
    map 'details', to: :ceramic_details
    map 'urn', to: :ceramic_urn
  end
end

# This is Lutaml::Model class that represents the collection of Ceramic objects
class CeramicCollection < Lutaml::Model::Serializable
  attribute :ceramics, Ceramic, collection: true

  key_value do
    map to: :ceramics, # All data goes to the `ceramics` attribute
      root_mappings: {
        # The key of an object in this collection is mapped to the ceramic_id
        # attribute of the Ceramic object.
        # (e.g. `vase1`, `bowl2`)
        ceramic_id: :key,
        ceramic_type: :type,
        ceramic_details: "details",
        ceramic_urn: ["urn", "primary"]
      }
  end
end

The output becomes:

> ceramics_collection = CeramicCollection.from_yaml(yaml)
=> #<CeramicCollection:0x0000000107a2cf30
  @ceramics=
    [#<Ceramic:0x0000000107a2cf30
      @ceramic_id="vase1",
      @ceramic_type="vase",
      @ceramic_details=
        #<CeramicDetails:0x0000000107a2cf30
          @name="Imperial Vase",
          @insignia="Tang Tianbao">,
      @ceramic_urn="urn:ceramic:vase:vase1">,
     #<Ceramic:0x0000000107a2cf30
      @ceramic_id="bowl2",
      @ceramic_type="bowl",
      @ceramic_details=
        #<CeramicDetails:0x0000000107a2cf30
          @name="18th Century Bowl",
          @insignia="Ming Wanli">
      @ceramic_urn="urn:ceramic:bowl:bowl2">]

> first_ceramic = ceramics_collection.ceramics.first
> puts first_ceramic.to_yaml
=>
# ---
# id: vase1
# type: vase
# details:
#   name: Imperial Vase
#   insignia: Tang Tianbao
# urn: urn:ceramic:vase:vase1

> new_collection = CeramicCollection.new(ceramics: [
    Ceramic.new(ceramic_id: "vase1",
                ceramic_type: "vase",
                ceramic_urn: "urn:ceramic:vase:vase1",
                ceramic_details: CeramicDetails.new(
                  name: "Imperial Vase", insignia: "Tang Tianbao")
               ),
    Ceramic.new(ceramic_id: "bowl2",
                ceramic_type: "bowl",
                ceramic_urn: "urn:ceramic:vase:bowl2",
                ceramic_details: CeramicDetails.new(
                  name: "18th Century Bowl", insignia: "Ming Wanli")
               )
  ])
> new_collection.to_yaml
>
# ---
# vase1:
#   type: vase
#   details:
#     name: Imperial Vase
#     insignia: Tang Tianbao
#   urn:
#     primary: urn:ceramic:vase:vase1
# bowl2:
#   type: bowl
#   details:
#     name: 18th Century Bowl
#     insignia: Ming Wanli
#   urn:
#     primary: urn:ceramic:bowl:bowl2
Case 3: Mapping the key and delegating value to an inner object

In this use case, the "key" of the keyed collection is moved into the collection object to an attribute, and the entire "value" of the keyed collection is mapped to another attribute of the collection object.

When the "key" is moved into the collection object, the following happens:

  • The "key" of the keyed collection maps to a particular attribute of the collection’s instance object.

  • The data of the "value" of the keyed collection will be entirely mapped into an attribute of the collection’s instance object.

  • The original mapping of the "value" attribute of the collection’s instance object is retained.

The root_mappings option should only contain two mappings, and the mappings must lead to both the :key and :value keywords.

Syntax:

class SomeKeyedCollection < Lutaml::Model::Serializable
  attribute :name_of_attribute, AttributeValueType, collection: true

  hsh | json | yaml | toml | key_value do
    map to: :name_of_attribute,
      root_mappings: {
        value_type_attribute_name_for_key: :key, (1)
        value_type_attribute_name_for_value: :value (2)
      }
  end
end

class AttributeValueType < Lutaml::Model::Serializable
  attribute :value_type_attribute_name_for_key, :string
  attribute :value_type_attribute_name_for_value, SomeObject
end
  1. The :key keyword specifies that the "key" of the keyed collection maps to the value_type_attribute_name_for_key attribute of the collection’s instance object (i.e. AttributeValueType).

  2. The :value keyword specifies that the entire "value" of the keyed collection maps to the value_type_attribute_name_for_value attribute of the collection’s instance object (i.e. SomeObject).

When the root_mappings mapping contains more than one mapping rule, the root_mappings mapping will override all other mappings in the collection object. This means that unmapped attributes in root_mappings will not be incorporated in the collection instance objects.

Example 45. Using map with root_mappings (key and value) to map a keyed collection into individual models

Given this data:

---
vase1:
  name: Imperial Vase
  insignia: "Tang Tianbao"
bowl2:
  name: 18th Century Bowl
  insignia: "Ming Wanli"

A model can be defined for this YAML as follows:

# This is a normal Lutaml::Model class
class CeramicDetails < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :insignia, :string

  key_value do
    map 'name', to: :name
    map 'insignia', to: :insignia
  end
end

# This is a normal Lutaml::Model class
class Ceramic < Lutaml::Model::Serializable
  attribute :ceramic_id, :string
  attribute :ceramic_details, CeramicDetails

  key_value do
    map 'id', to: :ceramic_id
    map 'details', to: :ceramic_details
  end
end

# This is Lutaml::Model class that represents the collection of Ceramic objects
class CeramicCollection < Lutaml::Model::Serializable
  attribute :ceramics, Ceramic, collection: true

  key_value do
    map to: :ceramics, # All data goes to the `ceramics` attribute
      root_mappings: {
        # The key of an object in this collection is mapped to the ceramic_id
        # attribute of the Ceramic object.
        # (e.g. `vase1`, `bowl2`)
        ceramic_id: :key,
        # The value of an object in this collection is mapped to the
        # ceramic_details attribute of the Ceramic object.
        # (e.g. `name: 18th Century Bowl`, `insignia: "Ming Wanli"`
        ceramic_details: :value
      }
  end
end
# Parsing the YAML collection with dynamic data keys
> ceramic_collection = CeramicCollection.from_yaml(yaml)
> #<CeramicCollection:0x0000000104ac7240
  @ceramics=
    [#<Ceramic:0x0000000104ac6e30
      @ceramic_id="vase1",
      @ceramic_details=
        #<CeramicDetails:0x0000000104ac6e30
          @name="Imperial Vase",
          @insignia="Tang Tianbao">,
     #<Ceramic:0x0000000104ac58f0
      @ceramic_id="bowl2",
      @ceramic_details=
        #<CeramicDetails:0x0000000104ac58f0
          @name="18th Century Bowl",
          @insignia="Ming Wanli">]

# NOTE: When an individual Ceramic object is serialized, the `id` attribute is
# the original key in the incoming YAML data.
> first_ceramic = ceramic_collection.ceramics.first
> puts first_ceramic.to_yaml
=>
# ---
# id: vase1
# details:
#   name: Imperial Vase
#   insignia: Tang Tianbao

# NOTE: When in a collection, the `ceramic_id` attribute is used to key the data,
# and it disappears from the individual object.
> puts ceramic_collection.to_yaml
=>
# ---
# vase1:
#   name: Imperial Vase
#   insignia: Tang Tianbao
# bowl2:
#   name: 18th Century Bowl
#   insignia: Ming Wanli

# NOTE: When the collection is serialized, the `ceramic_id` attribute is used to
# key the data. This is defined through the `map` with `root_mappings` method in
# CeramicCollection.
> new_collection = CeramicCollection.new(ceramics: [
    Ceramic.new(ceramic_id: "vase1",
                ceramic_details: CeramicDetails.new(
                  name: "Imperial Vase", insignia: "Tang Tianbao")
               ),
    Ceramic.new(ceramic_id: "bowl2",
                ceramic_details: CeramicDetails.new(
                  name: "18th Century Bowl", insignia: "Ming Wanli")
               )
  ])
> puts new_collection.to_yaml
=>
# ---
# vase1:
#   name: Imperial Vase
#   insignia: Tang Tianbao
# bowl2:
#   name: 18th Century Bowl
#   insignia: Ming Wanli

Attribute extraction

Note
This feature is for key-value data model serialization only.

The child_mappings option is used to extract results from a key-value serialization data model (Hash, JSON, YAML, TOML) into a Lutaml::Model::Serializable object (collection or not).

The values are extracted from the key-value data model using the list of keys provided.

Syntax:

class SomeObject < Lutaml::Model::Serializable
  attribute :name_of_attribute, AttributeValueType, collection: true

  hsh | json | yaml | toml | key_value do
    map 'key_value_model_attribute_name', to: :name_of_attribute,
      child_mappings: {
        value_type_attribute_name_1: (1)
          {path_to_value_1}, (2)
        value_type_attribute_name_2:
          {path_to_value_2},
        # ...
      }
  end
end
  1. The value_type_attribute_name_1 is the attribute name in the AttributeValueType model. The value of this attribute will be assigned the key of the hash in the key-value data model.

  2. The path_to_value_1 is an array of keys that represent the path to the value in the key-value serialization data model. The keys are used to extract the value from the key-value serialization data model and assign it to the attribute in the AttributeValueType model.

    The path_to_value is in a nested array format with each value a symbol or a string, where each symbol represents a key to traverse down. The last key in the path is the value to be extracted.

Example 46. Determining the path to value in a key-value data model

The following JSON contains 2 keys in schema named engine and gearbox.

{
  "components": {
    "engine": {
      "manufacturer": "Ford",
      "model": "V8"
    },
    "gearbox": {
      "manufacturer": "Toyota",
      "model": "4-speed"
    }
  }
}

The path to value for the engine schema is [:components, :engine] and for the gearbox schema is [:components, :gearbox].

In path_to_value, the :key and :value are reserved instructions used to assign the key or value of the serialization data respectively as the value to the attribute.

In the following JSON content, the path_to_value for the object keys named engine and gearbox will utilize the :key keyword to assign the key of the object as the value of a designated attribute.

{
  "components": {
    "engine": { /*...*/ },
    "gearbox": { /*...*/ }
  }
}

If a specified value path is not found, the corresponding attribute in the model will be assigned a nil value.

Example 47. Attribute values set to nil when the path_to_value is not found

In the following JSON content, the path_to_value of [:extras, :sunroof] and [:extras, :drinks_cooler] at the object "gearbox" would be set to nil.

{
  "components": {
    "engine": {
      "manufacturer": "Ford",
      "extras": {
        "sunroof": true,
        "drinks_cooler": true
      }
    },
    "gearbox": {
      "manufacturer": "Toyota"
    }
  }
}
Example 48. Using the child_mappings option to extract values from a key-value data model

The following JSON contains 2 keys in schema named foo and bar.

{
  "schemas": {
    "foo": { (1)
      "path": { (2)
        "link": "link one",
        "name": "one"
      }
    },
    "bar": { (1)
      "path": { (2)
        "link": "link two",
        "name": "two"
      }
    }
  }
}
  1. The keys foo and bar are to be mapped to the id attribute.

  2. The nested path.link and path.name keys are used as the link and name attributes, respectively.

A model can be defined for this JSON as follows:

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

class ChildMappingClass < Lutaml::Model::Serializable
  attribute :schemas, Schema, collection: true

  json do
    map "schemas", to: :schemas,
                   child_mappings: {
                     id: :key,
                     link: %i[path link],
                     name: %i[path name],
                   }
  end
end

The output becomes:

> ChildMappingClass.from_json(json)
> #<ChildMappingClass:0x0000000104ac7240
 @schemas=
  [#<Schema:0x0000000104ac6e30 @id="foo", @link="link one", @name="one">,
   #<Schema:0x0000000104ac58f0 @id="bar", @link="link two", @name="two">]>
> ChildMappingClass.new(schemas: [Schema.new(id: "foo", link: "link one", name: "one"), Schema.new(id: "bar", link: "link two", name: "two")]).to_json
> #{"schemas"=>{"foo"=>{"path"=>{"link"=>"link one", "name"=>"one"}}, {"bar"=>{"path"=>{"link"=>"link two", "name"=>"two"}}}}}

In this example:

  • The key of each schema (foo and bar) is mapped to the id attribute.

  • The nested path.link and path.name keys are mapped to the link and name attributes, respectively.

Format-independent mechanisms

Mapping value transformation

A mapping value transformation is used when the value of an attribute needs to be transformed around the serialization process. Collection attributes are also supported.

This is useful when the representation of the value in a serialization format differs from its internal representation in the model.

Note
Value transformation can be applied at the attribute-level or at the serialization-mapping level. They can also be applied together.

Syntax:

class SomeObject < Lutaml::Model::Serializable
  # Attribute-level transformation
  attribute :attribute_name, {attr_type}, transform: { (1)
    export: ->(value) { ... },
    import: ->(value) { ... }
  }

  # Mapping-level transformation in JSON format
  {key_value_formats} do
    map "key", to: :attribute_name, transform: { (2)
      export: ->(value) { ... },
      import: ->(value) { ... }
    }
  end

  # Mapping-level transformation in XML format
  xml do
    map_element "ElementName", to: :attribute_name, transform: { (3)
      export: ->(value) { ... },
      import: ->(value) { ... }
    }

    map_attribute "AttributeName", to: :attribute_name, transform: {
      export: ->(value) { ... },
      import: ->(value) { ... }
    }
  end
end
  1. At the attribute level, the transform option applied to the attribute method is used to define the transformation for the attribute.

  2. At the mapping level (for {key_value_formats} formats), the transform option applied to the map method is used to define the transformation for the mapping.

  3. At the mapping level (for the XML format), the transform option applied to the map_* methods is used to define the transformation for the mapping.

Where,

attribute_name

The name of the attribute.

attr_type

The type of the attribute.

Attribute-level transform

The option to define a transformation for the attribute value.

Attribute-level export

The transformation Proc for the value when it is being retrieved from the model.

Attribute-level import

The transformation Proc for the value when it is being assigned to the model.

{key_value_formats}

The serialization format (e.g. hsh, json, yaml, toml, key_value) for which the mapping is defined.

Mapping-level transform

The option to define a transformation for the serialization mapping value. The value given to the Proc is the model attribute value that does not go through attribute-level transform.

Mapping-level export

The transformation Proc for the attribute value when it is being written to the serialization format.

Mapping-level import

The transformation Proc for the value when it is being read from the serialization format and assigned to the model.

class Ceramic < Lutaml::Model::Serializable
  attribute :glaze_type, :string

  # Mapping-level transformation in key-value formats
  json do
    map "glazeType", to: :glaze_type, transform: {
      export: ->(value) { "Traditional #{value}" },
      import: ->(value) { value.gsub("Traditional ", "") }
    }
  end

  # Mapping-level transformation in XML format
  xml do
    root "Ceramic"
    map_attribute "glaze-type", to: :glaze_type, transform: {
      export: ->(value) { "Traditional #{value}" },
      import: ->(value) { value.gsub("Traditional ", "") }
    }
  end
end
ceramic = Ceramic.new(glaze_type: "celadon")

# Export transformation applied on defined mapping
ceramic.to_json
# => {"glazeType": "Traditional celadon"}

# Export transformation applied on defined mapping
ceramic.to_xml
# => <Ceramic glaze-type="Traditional celadon"/>

# No export transformation when no mapping exists
ceramic.to_yaml
# => glaze_type: "celadon"

# Import transformation applied on defined mapping
ceramic = Ceramic.from_json('{ "glazeType" => "Traditional celadon" }')
ceramic.glaze_type
# => "celadon"

# Import transformation applied on defined mapping
ceramic = Ceramic.from_xml('<Ceramic glaze-type="Traditional raku"/>')
ceramic.glaze_type
# => "raku"

# No import transformation when no mapping exists
ceramic = Ceramic.from_yaml('glaze_type: "Traditional celadon"')
ceramic.glaze_type
# => "Traditional celadon"

Attribute-level and mapping-level transformations can be used together for the same attribute in a chained fashion.

Precedence applies to the two levels of transformation for deserialization:

  1. Mapping-level transformation, if defined, occurs first

  2. Attribute-level transformation, if defined, is applied to the result of the mapping-level transformation

Conversely, precedence applies in the same order for serialization:

  1. Attribute-level transformation, if defined, occurs first

  2. Mapping-level transformation, if defined, is applied to the result of the attribute-level transformation

This mechanism allows for flexible value transformations without needing format-specific custom methods.

Diagram indicating flow of transformation across layers
╔════════════════════════════╗       ╔════════════════════════════╗
║ Serialization Format Value ║       ║ Serialization Format Value ║
╚════════════════════════════╝       ╚════════════════════════════╝
              |                                    ▲
              ▼                                    |
╔════════════════════════════╗       ╔════════════════════════════╗
║      Mapping Transform     ║       ║      Mapping Transform     ║
╚════════════════════════════╝       ╚════════════════════════════╝
              |                                    ▲
              ▼                                    |
╔════════════════════════════╗       ╔════════════════════════════╗
║     Attribute Transform    ║       ║     Attribute Transform    ║
╚════════════════════════════╝       ╚════════════════════════════╝
              |                                    ▲
              ▼                                    |
╔════════════════════════════╗       ╔════════════════════════════╗
║    Model Attribute Value   ║       ║    Model Attribute Value   ║
╚════════════════════════════╝       ╚════════════════════════════╝
class Ceramic < Lutaml::Model::Serializable
  # Attribute-level transformation
  attribute :glaze_type, :string, transform: {
    export: ->(value) { "Ceramic #{value}" },
    import: ->(value) { value.gsub("Ceramic ", "") }
  }

  # Mapping-level transformation in key-value formats
  json do
    map "glazeType", to: :glaze_type, transform: {
      export: ->(value) { "Traditional #{value}" },
      import: ->(value) { value.gsub("Traditional ", "") }
    }
  end

  # Mapping-level transformation in XML format
  xml do
    root "Ceramic"
    map_attribute "glaze-type", to: :glaze_type, transform: {
      export: ->(value) { "Traditional #{value}" },
      import: ->(value) { value.gsub("Traditional ", "") }
    }
  end
end
ceramic = Ceramic.new(glaze_type: "Ceramic celadon")

# Attribute-level export transformation applied
ceramic.glaze_type
# => "Ceramic celadon"

# Internal representation
ceramic.instance_value_get(:@glaze_type)
# => "celadon"

# Mapping-level export transformation applied to attribute-level transformed value
ceramic.to_json
# => {"glazeType": "Traditional Ceramic celadon"}

# Mapping-level export transformation applied to attribute-level transformed value
ceramic.to_xml
# => <Ceramic glaze-type="Traditional Ceramic celadon"/>

# No mapping-level export transformation when no mapping exists
ceramic.to_yaml
# => glaze_type: "Ceramic celadon"

# Attribute-level import transformation applied to mapping-level transformed value
ceramic = Ceramic.from_json('{ "glazeType" => "Traditional Ceramic celadon" }')
ceramic.glaze_type
# => "Ceramic celadon"

# Attribute-level import transformation applied to mapping-level transformed value
ceramic = Ceramic.from_xml('<Ceramic glaze-type="Traditional Ceramic raku"/>')
ceramic.glaze_type
# => "Ceramic raku"

# No mapping-level import transformation when no mapping exists
ceramic = Ceramic.from_yaml('glaze_type: "Ceramic celadon"')
ceramic.glaze_type
# => "Ceramic celadon"

Separate data model class

The Serialize module can be used to define only serialization mappings for a separately defined data model class (a Ruby class).

Note
This is traditionally called "custom model".

Syntax:

class MappingClass < Lutaml::Model::Serializable
  model {DataModelClass}

  # ...
end

Where,

MappingClass

The class that represents the serialization mappings. This class must be a subclass of Lutaml::Model::Serializable.

DataModelClass

The class that represents the data model.

When using a separate data model class, it is important to remember that the serialization methods (instance#to_*, klass.from_*, such as instance.to_yaml, instance.to_xml or Klass.from_yaml, Klass.from_xml), are to be called on the mapping class, not the data model instance.

Example 49. Using the model method to define serialization mappings for a separate model
class Ceramic
  attr_accessor :type, :glaze

  def name
    "#{type} with #{glaze}"
  end
end

class CeramicSerialization < Lutaml::Model::Serializable
  model Ceramic

  xml do
    map_element 'type', to: :type
    map_element 'glaze', to: :glaze
  end
end
> Ceramic.new(type: "Porcelain", glaze: "Clear").name
> # "Porcelain with Clear"
> CeramicSerialization.from_xml(xml)
> #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze="Clear">
> Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml
> #<Ceramic><type>Porcelain</type><glaze>Clear</glaze></Ceramic>
Example 50. Using the model method to define serialization mappings for a separate model in a model hierarchy

The following class will parse the XML snippet below:

class CustomModelChild
  attr_accessor :street, :city
end

class CustomModelChildMapper < Lutaml::Model::Serializable
  model CustomModelChild

  attribute :street, Lutaml::Model::Type::String
  attribute :city, Lutaml::Model::Type::String

  xml do
    map_element :street, to: :street
    map_element :city, to: :city
  end
end

class CustomModelParentMapper < Lutaml::Model::Serializable
  attribute :first_name, Lutaml::Model::Type::String
  attribute :child_mapper, CustomModelChildMapper

  xml do
    map_element :first_name, to: :first_name
    map_element :CustomModelChild,
                with: { to: :child_to_xml, from: :child_from_xml }
  end

  def child_to_xml(model, parent, doc)
    child_el = doc.create_element("CustomModelChild")
    street_el = doc.create_element("street")
    city_el = doc.create_element("city")

    doc.add_text(street_el, model.child_mapper.street)
    doc.add_text(city_el, model.child_mapper.city)

    doc.add_element(child_el, street_el)
    doc.add_element(child_el, city_el)
    doc.add_element(parent, child_el)
  end

  def child_from_xml(model, value)
    model.child_mapper ||= CustomModelChild.new

    model.child_mapper.street = value["elements"]["street"].text
    model.child_mapper.city = value["elements"]["city"].text
  end
end
<CustomModelParent>
  <first_name>John</first_name>
  <CustomModelChild>
    <street>Oxford Street</street>
    <city>London</city>
  </CustomModelChild>
</CustomModelParent>
> instance = CustomModelParentMapper.from_xml(xml)
> #<CustomModelParent:0x0000000107c9ca68 @child_mapper=#<CustomModelChild:0x0000000107c95218 @city="London", @street="Oxford Street">, @first_name="John">
> CustomModelParentMapper.to_xml(instance)
> #<CustomModelParent><first_name>John</first_name><CustomModelChild><street>Oxford Street</street><city>London</city></CustomModelChild></CustomModelParent>

Rendering default values (forced rendering of default values)

By default, attributes with default values are not rendered if the current value is the same as the default value.

In certain cases, it is necessary to render the default value even if the current value is the same as the default value. This is achieved by setting the render_default option to true.

Syntax:

attribute :name_of_attribute, Type, default: -> { value }

xml do
  map_element 'name_of_attribute', to: :name_of_attribute, render_default: true
  map_attribute 'name_of_attribute', to: :name_of_attribute, render_default: true
end

hsh | json | yaml | toml | key_value do
  map 'name_of_attribute', to: :name_of_attribute, render_default: true
end
Example 51. Using the render_default option to force encoding the default value
class Glaze < Lutaml::Model::Serializable
  attribute :color, :string, default: -> { 'Clear' }
  attribute :opacity, :string, default: -> { 'Opaque' }
  attribute :temperature, :integer, default: -> { 1050 }
  attribute :firing_time, :integer, default: -> { 60 }

  xml do
    root "glaze"
    map_element 'color', to: :color
    map_element 'opacity', to: :opacity, render_default: true
    map_attribute 'temperature', to: :temperature
    map_attribute 'firingTime', to: :firing_time, render_default: true
  end

  json do
    map 'color', to: :color
    map 'opacity', to: :opacity, render_default: true
    map 'temperature', to: :temperature
    map 'firingTime', to: :firing_time, render_default: true
  end
end
Example 52. Attributes with render_default: true are rendered when the value is identical to the default
> glaze_new = Glaze.new
> puts glaze_new.to_xml
# <glaze firingTime="60">
#   <opacity>Opaque</opacity>
# </glaze>
> puts glaze_new.to_json
# {"firingTime":60,"opacity":"Opaque"}
Example 53. Attributes with render_default: true with non-default values are rendered
> glaze = Glaze.new(color: 'Celadon', opacity: 'Semitransparent', temperature: 1300, firing_time: 90)
> puts glaze.to_xml
# <glaze color="Celadon" temperature="1300" firingTime="90">
#   <opacity>Semitransparent</opacity>
# </glaze>
> puts glaze.to_json
# {"color":"Celadon","temperature":1300,"firingTime":90,"opacity":"Semitransparent"}

Advanced attribute mapping

Mapping multiple names to a single attribute

The mapping methods support multiple names mapping to a single attribute using an array of names.

Syntax:

hsh | json | yaml | toml | key_value do
  map ["name1", "name2"], to: :attribute_name
end

xml do
  map_element ["name1", "name2"], to: :attribute_name
  map_attribute ["attr1", "attr2"], to: :attribute_name
end

When serializing, the first element in the array of mapped names is always used as the output name.

Example 54. Using multiple names to map to a single attribute
class CustomModel < Lutaml::Model::Serializable
  attribute :full_name, Lutaml::Model::Type::String
  attribute :color, Lutaml::Model::Type::String
  attribute :id, Lutaml::Model::Type::String

  json do
    map ["name", "custom_name"], with: { to: :name_to_json, from: :name_from_json }
    map ["color", "shade"], with: { to: :color_to_json, from: :color_from_json }
  end

  xml do
    root "CustomModel"
    map_element ["name", "custom-name"], with: { to: :name_to_xml, from: :name_from_xml }
    map_element ["color", "shade"], with: { to: :color_to_xml, from: :color_from_xml }
    map_attribute ["id", "identifier"], to: :id
  end

  # Custom methods for JSON
  def name_to_json(model, doc)
    doc["name"] = "JSON Model: #{model.full_name}"
  end

  def name_from_json(model, value)
    model.full_name = value&.sub(/^JSON Model: /, "")
  end

  def color_to_json(model, doc)
    doc["color"] = model.color.upcase
  end

  def color_from_json(model, value)
    model.color = value&.downcase
  end

  # Custom methods for XML
  def name_to_xml(model, parent, doc)
    el = doc.create_element("name")
    doc.add_text(el, "XML Model: #{model.full_name}")
    doc.add_element(parent, el)
  end

  def name_from_xml(model, value)
    model.full_name = value.sub(/^XML Model: /, "")
  end

  def color_to_xml(model, parent, doc)
    el = doc.create_element("color")
    doc.add_text(el, model.color.upcase)
    doc.add_element(parent, el)
  end

  def color_from_xml(model, value)
    model.color = value.downcase
  end
end

For JSON:

{
  "custom_name": "JSON Model: Vase",
  "shade": "BLUE",
  "identifier": "123"
}

For XML:

<CustomModel id="123">
  <name>XML Model: Vase</name>
  <color>BLUE</color>
</CustomModel>
> model = CustomModel.from_json(json)
> model.full_name
> # "Vase"
> model.color
> # "blue"

Attribute mapping delegation

Delegate attribute mappings to nested objects using the delegate option.

Syntax:

xml | hsh | json | yaml | toml do
  map 'key_value_model_attribute_name', to: :name_of_attribute, delegate: :model_to_delegate_to
end
Example 55. Using the delegate option to map attributes to nested objects

The following class will parse the JSON snippet below:

class Glaze < Lutaml::Model::Serializable
  attribute :color, :string
  attribute :temperature, :integer

  json do
    map 'color', to: :color
    map 'temperature', to: :temperature
  end
end

class Ceramic < Lutaml::Model::Serializable
  attribute :type, :string
  attribute :glaze, Glaze

  json do
    map 'type', to: :type
    map 'color', to: :color, delegate: :glaze
  end
end
{
  "type": "Porcelain",
  "color": "Clear"
}
> Ceramic.from_json(json)
> #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze=#<Glaze:0x0000000104ac7240 @color="Clear", @temperature=nil>>
> Ceramic.new(type: "Porcelain", glaze: Glaze.new(color: "Clear")).to_json
> #{"type"=>"Porcelain", "color"=>"Clear"}
Note
The corresponding keyword used by Shale is receiver: instead of delegate:.

Attribute serialization with custom methods

General

Define custom methods for specific attribute mappings using the with: key for each serialization mapping block for from and to.

XML serialization with custom methods

Syntax:

XML serialization with custom methods
xml do
  map_element 'element_name', to: :name_of_element, with: {
    to: :method_name_to_serialize,
    from: :method_name_to_deserialize
  }
  map_attribute 'attribute_name', to: :name_of_attribute, with: {
    to: :method_name_to_serialize,
    from: :method_name_to_deserialize
  }
  map_content, to: :name_of_content, with: {
    to: :method_name_to_serialize,
    from: :method_name_to_deserialize
  }
end
Example 56. Using the with: key to define custom serialization methods for XML

The following class will parse the XML snippet below:

class Metadata < Lutaml::Model::Serializable
  attribute :category, :string
  attribute :identifier, :string
end

class CustomCeramic < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :size, :integer
  attribute :description, :string
  attribute :metadata, Metadata

  xml do
    map_element "Name", to: :name, with: { to: :name_to_xml, from: :name_from_xml }
    map_attribute "Size", to: :size, with: { to: :size_to_xml, from: :size_from_xml }
    map_content with: { to: :description_to_xml, from: :description_from_xml }
    map_element :metadata, to: :metadata, with: { to: :metadata_to_xml, from: :metadata_from_xml }
  end

  def name_to_xml(model, parent, doc)
    el = doc.create_element("Name")
    doc.add_text(el, "XML Masterpiece: #{model.name}")
    doc.add_element(parent, el)
  end

  def name_from_xml(model, value)
    model.name = value.sub(/^XML Masterpiece: /, "")
  end

  def size_to_xml(model, parent, doc)
    doc.add_attribute(parent, "Size", model.size + 3)
  end

  def size_from_xml(model, value)
    model.size = value.to_i - 3
  end

  def description_to_xml(model, parent, doc)
    doc.add_text(parent, "XML Description: #{model.description}")
  end

  def description_from_xml(model, value)
    model.description = value.join.strip.sub(/^XML Description: /, "")
  end

  def metadata_to_xml(model, parent, doc)
    metadata_el = doc.create_element("metadata")
    category_el = doc.create_element("category")
    identifier_el = doc.create_element("identifier")

    doc.add_text(category_el, model.metadata.category)
    doc.add_text(identifier_el, model.metadata.identifier)

    doc.add_element(metadata_el, category_el)
    doc.add_element(metadata_el, identifier_el)
    doc.add_element(parent, metadata_el)
  end

  def metadata_from_xml(model, value)
    model.metadata ||= Metadata.new

    model.metadata.category = value["elements"]["category"].text
    model.metadata.identifier = value["elements"]["identifier"].text
  end
end
<CustomCeramic Size="15">
  <Name>XML Masterpiece: Vase</Name>
  XML Description: A beautiful ceramic vase
  <metadata>
    <category>Metadata</category>
    <identifier>123</identifier>
  </metadata>
</CustomCeramic>
> CustomCeramic.from_xml(xml)
> #<CustomCeramic:0x0000000108d0e1f8
   @element_order=["text", "Name", "text", "Size", "text"],
   @name="Masterpiece: Vase",
   @ordered=nil,
   @size=12,
   @description="A beautiful ceramic vase",
   @metadata=#<Metadata:0x0000000105ad52e0 @category="Metadata", @identifier="123">>
> puts CustomCeramic.new(name: "Vase", size: 12, description: "A beautiful vase", metadata: Metadata.new(category: "Glaze", identifier: 15)).to_xml
# <CustomCeramic Size="15">
#   <Name>XML Masterpiece: Vase</Name>
#   <metadata>
#     <category>Glaze</category>
#     <identifier>15</identifier>
#   </metadata>
#   XML Description: A beautiful vase
# </CustomCeramic>
def custom_method_from_xml(model, value)
  instance = value.node # Lutaml::Model::XmlAdapter::AdapterElement
  # OR
  instance = value.node.adapter_node # Adapter::Element

  xml = instance.to_xml
end

When building a model from XML in custom methods, if the value parameter is a mapping_hash, then it allows access to the parsed XML structure through value.node which can be converted to an XML string using to_xml.

Note
For NokogiriAdapter, we can also call to_xml on value.node.adapter_node.
> value
> # {"text"=>["\n    ", "\n    ", "\n  "], "elements"=>{"category"=>{"text"=>"Metadata"}}}
> value.to_xml
> # undefined_method `to_xml`

> value.node

# Nokogiri Adapter Node

#<Lutaml::Model::XmlAdapter::NokogiriElement:0x0000000107656ed8
#   @attributes={},
#   @children=
#   [#<Lutaml::Model::XmlAdapter::NokogiriElement:0x0000000107656cd0 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n    ">,
#   #<Lutaml::Model::XmlAdapter::NokogiriElement:0x00000001076569b0
#     @attributes={},
#     @children=
#     [#<Lutaml::Model::XmlAdapter::NokogiriElement:0x00000001076567f8 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="Metadata">],
#     @default_namespace=nil,
#     @name="category",
#     @namespace_prefix=nil,
#     @text="Metadata">,
#   #<Lutaml::Model::XmlAdapter::NokogiriElement:0x0000000107656028 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n  ">],
# @default_namespace=nil,
# @name="metadata",
# @namespace_prefix=nil,
# @text="\n    Metadata\n  ">

# Ox Adapter Node

#<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584f78
# @attributes={},
# @children=
#   [#<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584e60
#     @attributes={},
#     @children=[#<Lutaml::Model::XmlAdapter::OxElement:0x0000000107584d48 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="Metadata">],
#     @default_namespace=nil,
#     @name="category",
#     @namespace_prefix=nil,
#     @text="Metadata">],
# @default_namespace=nil,
# @name="metadata",
# @namespace_prefix=nil,
# @text=nil>

# Oga Adapter Node

# <Lutaml::Model::XmlAdapter::Oga::Element:0x0000000107314158
# @attributes={},
# @children=
#   [#<Lutaml::Model::XmlAdapter::Oga::Element:0x0000000107314090 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n    ">,
#   #<Lutaml::Model::XmlAdapter::Oga::Element:0x000000010730fe78
#     @attributes={},
#     @children=[#<Lutaml::Model::XmlAdapter::Oga::Element:0x000000010730fd88 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="Metadata">],
#     @default_namespace=nil,
#     @name="category",
#     @namespace_prefix=nil,
#     @text="Metadata">,
#   #<Lutaml::Model::XmlAdapter::Oga::Element:0x000000010730f8d8 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n  ">],
# @default_namespace=nil,
# @name="metadata",
# @namespace_prefix=nil,
# @text="\n    Metadata\n  ">

> value.node.to_xml
> #<metadata><category>Metadata</category></metadata>
Key-value data model serialization with custom methods
Key-value data model serialization with custom methods
hsh | json | yaml | toml do
  map 'attribute_name', to: :name_of_attribute, with: {
    to: :method_name_to_serialize,
    from: :method_name_to_deserialize
  }
end
Example 57. Using the with: key to define custom serialization methods

The following class will parse the JSON snippet below:

class CustomCeramic < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :size, :integer

  json do
    map 'name', to: :name, with: { to: :name_to_json, from: :name_from_json }
    map 'size', to: :size
  end

  def name_to_json(model, doc)
    doc["name"] = "Masterpiece: #{model.name}"
  end

  def name_from_json(model, value)
    model.name = value.sub(/^Masterpiece: /, '')
  end
end
{
  "name": "Masterpiece: Vase",
  "size": 12
}
> CustomCeramic.from_json(json)
> #<CustomCeramic:0x0000000104ac7240 @name="Vase", @size=12>
> CustomCeramic.new(name: "Vase", size: 12).to_json
> #{"name"=>"Masterpiece: Vase", "size"=>12}

Handling the missing values family

General

Different information models define different primitive value types, and the same goes for the notions of the "missing values" family:

the empty value

the value is present but empty

the non-existent value

the value is not present

the undefined value

the value is not defined

There are also different ways to represent these missing values when the attribute accepts a single value or a collection of values.

Table 2. Support of missing value types in different technologies
Technology Missing value type Realized as

Lutaml::Model

empty value

Ruby empty string ("")

non-existent value

Ruby NilClass (nil)

undefined value

class Uninitialized

XML element

empty value

XML blank element: <status></status> or <status/>

non-existent value

XML blank element with attribute xsi:nil: <status xsi:nil="true"/>

undefined value

the XML element is omitted

XML attribute

empty value

XML blank attribute: status=""

non-existent value

the XML attribute is omitted

undefined value

the XML attribute is omitted

JSON

empty value

JSON empty string ("")

non-existent value

JSON null value

undefined value

the JSON key is omitted

TOML

empty value

TOML empty string

non-existent value

the TOML key is omitted since TOML does not support the concept of null.

undefined value

the TOML key is omitted

Note
The Uninitialized class is a special Lutaml::Model construct, it is not supported by normal Ruby objects.

The challenge for the developer is how to represent fully compatible semantics using interoperable data models across different technologies.

Lutaml::Model provides you with several mechanisms to retain the missing values semantics. An example mapping is shown in the following diagram.

Mapping of missing value types between Lutaml::Model and YAML
┌─────────────────────────────────────────────────────────────────────────────┐
│  ╔════════════════════════╗                    ╔════════════════════════╗   │
│  ║  Lutaml::Model Values  ║                    ║      YAML Values       ║   │
│  ╠════════════════════════╣                    ╠════════════════════════╣   │
│  ║                        ║      mapping       ║                        ║   │
│  ║  "empty"               ║◀──────────────────▶║  "empty"               ║   │
│  ║  (empty string, [])    ║      to empty      ║  (empty string, [])    ║   │
│  ║                        ║                    ║                        ║   │
│  ╟────────────────────────╢                    ╟────────────────────────╢   │
│  ║                        ║      mapping       ║                        ║   │
│  ║  "non-existent"        ║◀──────────────────▶║  "non-existent"        ║   │
│  ║  (nil)                 ║  to non-existent   ║  (null)                ║   │
│  ║                        ║                    ║                        ║   │
│  ╟────────────────────────╢                    ╟────────────────────────╢   │
│  ║                        ║      mapping       ║                        ║   │
│  ║  "undefined"           ║◀──────────────────▶║  "undefined"           ║   │
│  ║  (uninitialized)       ║   to undefined     ║  (key omitted)         ║   │
│  ║                        ║                    ║                        ║   │
│  ╚════════════════════════╝                    ╚════════════════════════╝   │
└─────────────────────────────────────────────────────────────────────────────┘

In the case where the interoperating technologies do not support the full spectrum of missing value types, it is necessary for the developer to understand any such behavior and relevant handling.

Mapping of missing value types between Lutaml::Model and TOML
┌─────────────────────────────────────────────────────────────────────────────┐
│  ╔════════════════════════╗                    ╔════════════════════════╗   │
│  ║  Lutaml::Model Values  ║                    ║      TOML Values       ║   │
│  ╠════════════════════════╣                    ╠════════════════════════╣   │
│  ║                        ║      mapping       ║                        ║   │
│  ║  "empty"               ║◀──────────────────▶║  "empty"               ║   │
│  ║  (empty string, [])    ║      to empty      ║  (empty string, [])    ║   │
│  ║                        ║                    ║                        ║   │
│  ╟────────────────────────╢                    ╟────────────────────────╢   │
│  ║                        ║      mapping       ║                        ║   │
│  ║  "non-existent"        ║◀──────────────────▶║                        ║   │
│  ║  (nil)                 ║   to undefined     ║  "undefined"           ║   │
│  ║                        ║                    ║  (key omitted)         ║   │
│  ╟────────────────────────╢                    ║                        ║   │
│  ║                        ║      mapping       ║                        ║   │
│  ║  "undefined"           ║─────(one-way)─────▶║     TOML does not      ║   │
│  ║  (uninitialized)       ║   to undefined     ║     support NULL       ║   │
│  ║                        ║                    ║                        ║   │
│  ╚════════════════════════╝                    ╚════════════════════════╝   │
└─────────────────────────────────────────────────────────────────────────────┘

There are the following additional challenges that a developer must take into account of:

  • Single attribute value vs collection attribute value. Different technologies treat single/collection values differently.

  • External schemas and systems that interoperate with serializations from Lutaml::Model. Many schemas and systems adopt "different" conventions for representing missing value semantics (sometimes very awkward ones).

The solution for the first challenge is to understand the behavior of the different technologies used. The default mappings are described in Value representation in Lutaml::Model and Value representation in serialization formats.

Value representation in Lutaml::Model

The following table summarizes the behavior of the Lutaml::Model in regards of the "missing values" family.

Table 3. Handling of missing value types in Lutaml::Model data types
LutaML value type Cardinality (1 or n) Missing value type Ruby value

Collection attribute

collection

empty value

[] (Array)

non-existent value

nil (NilClass)

undefined value

No assigned value

:string

single

empty value

"" (String)

non-existent value

nil (NilClass)

undefined value

No assigned value

:integer

single

empty value

N/A

non-existent value

nil (NilClass)

undefined value

No assigned value

:float

single

empty value

N/A

non-existent value

nil (NilClass)

undefined value

No assigned value

:boolean

single

empty value

N/A

non-existent value

nil (NilClass)

undefined value

No assigned value

:date

single

empty value

N/A

non-existent value

nil (NilClass)

undefined value

No assigned value

:time_without_date

single

empty value

N/A

non-existent value

nil (NilClass)

undefined value

No assigned value

:date_time

single

empty value

N/A

non-existent value

nil (NilClass)

undefined value

No assigned value

:time

single

empty value

N/A

non-existent value

nil (NilClass)

undefined value

No assigned value

:decimal

single

empty value

N/A

non-existent value

nil (NilClass)

undefined value

No assigned value

:hash

single

empty value

{} (Hash)

non-existent value

nil (NilClass)

undefined value

No assigned value

Value representation in serialization formats

Every serialization format uses a different information model to represent these missing values.

Some serialization formats support all 3 types of missing values, while others only support a subset of them.

Table 4. Varied handling of missing values in supported serialization formats
Serialization format Cardinality (1 or n) Missing value type Example

XML

collection

empty collection

the XML blank element: <status></status> or <status/>

non-existent collection

a blank element with attribute xsi:nil: <status xsi:nil="true"/>

undefined collection

the XML element is omitted

single

empty value

the XML blank element: <status></status> or <status/>

non-existent value

a blank element with attribute xsi:nil: <status xsi:nil="true"/>

undefined value

the XML element is omitted

JSON

collection

empty collection

an empty array ([])

non-existent collection

the value null

undefined collection

the key is omitted

single

empty value

an empty string ("")

non-existent value

the value null

undefined value

the key is omitted

YAML

collection

empty collection

an empty array ([])

non-existent collection

the value null

undefined collection

the key is omitted

single

empty value

an empty string ("")

non-existent value

the value null

undefined value

the key is omitted

TOML

collection

empty collection

an empty array ([])

non-existent collection

TOML does not support the concept of "null"

undefined collection

the key is omitted

single

empty value

an empty string ("")

non-existent value

TOML does not support the concept of "null"

undefined value

the key is omitted

Missing value mapping

General

Lutaml::Model provides a comprehensive way to handle the missing values family across different serialization formats.

The value_map option as applied to serialization mapping rules allow users to meticulously define how each and every missing value should be mapped from a serialization format to a Lutaml::Model object.

The value_map option is used to define mappings for both from and to values:

from pairs

A hash of key-value pairs that determines the mapping of a missing value at the serialization format ("from") to a LutaML Model missing value where this mapping applies. The key is the missing value type in the serialization format, and the value is the missing value type in the LutaML Model.

Note
In other words, used when converting the serialized format into a Lutaml::Model Ruby object.
to pairs

A hash of key-value pairs that determines the mapping of a LutaML Model ("to") missing to a missing value choice at the serialization format where this mapping applies. The key is the missing value type in the LutaML Model, and the value is the missing value type in the serialization format.

Note
In other words, used when converting a Lutaml::Model Ruby object into the serialized format.

Syntax:

{map_command} 'format-key', to: :attribute_name, value_map: { (1)
  from: {
    {format-missing-value-n}: {model-missing-value-n}, (2)
    {format-missing-value-m}: {model-missing-value-m},
    {format-missing-value-o}: {model-missing-value-o}
  },
  to: {
    {model-missing-value-n}: {format-missing-value-n}, (3)
    {model-missing-value-m}: {format-missing-value-m},
    {model-missing-value-o}: {format-missing-value-o}
  }
}
  1. The {map_command} is a mapping rule with the actual command depending on the serialization format.

  2. In the from mapping, the keys are the missing value types in the serialization format.

  3. In the to mapping, the keys are the missing value types in the LutaML Model.

The missing value type mapping differs per serialization format, as serialization formats may not fully support all missing value types.

The availability of from and to keys and values depend on the types of missing values supported by that particular serialization format.

The available values for from and to for serialization formats are presented below, where the allowed values are to be used in the direction of the format. That means if the format supports :empty, it can be used as a key in from: direction, and the value in the to: direction (see {format-missing-value-n}) in the syntax.

Table 5. Available missing value types in different mapping commands
Map command Missing value types available (key in from: direction, value in the to: direction)

XML element map_element

:empty, :omitted, :nil

XML attribute map_attribute

:empty, :omitted

Hash map, map_content

:empty, :omitted, :nil

JSON map, map_content

:empty, :omitted, :nil

YAML map, map_content

:empty, :omitted, :nil

TOML map, map_content

:empty, :omitted

For instance, TOML does not support the notion of "null" and therefore the missing value type of nil cannot be used; therefore in a from: value map, it is not possible to indicate nil: {model-missing-value}.

In an XML mapping block, it is possible to do the following.

xml do
  map_element 'status', to: :status, value_map: {
    from: { empty: :nil, omitted: :omitted, nil: :nil },
    to: { empty: :nil, omitted: :omitted, nil: :nil }
  }
end

Each serialization format has specific behavior when handling values such as empty, omitted, and nil.

Users can specify the mapping for both from and to values using the value_map option in the attribute definition.

  • The keys that can be used in the from and to mappings are empty, omitted, and nil.

  • The values in the mappings can also be empty, omitted, and nil.

Note
Since nil is not supported in TOML, so mappings like nil: {any_option} or {any_option}: :nil will not work in TOML.
Note
In a collection attribute, the values of value_map also depend on the initialize_empty setting, where an omitted value in the serialization format can still lead to a nil or an empty array [] at the attribute-level (instead of the mapping-level).
Default value maps for serialization formats

The table below describes the default value_map configurations for supported serialization formats.

Default value map for XML element (single attribute)
attribute :attr, :string

xml do
  map_element 'key', to: :attr, value_map: {
    from: { empty: :nil, omitted: :omitted, nil: :nil },
    to: { empty: :empty, omitted: :omitted, nil: :nil }
  }
end
Table 6. Default missing value mapping configuration for single attributes in XML elements

Direction

Map rule

XML source

Model target

from:

empty: :nil

blank XML element (<status/>)

nil

omitted: :omitted

absent XML element

omitted from the model

nil: :nil

blank XML element with attribute xsi:nil (<status xsi:nil=true/>)

nil value in the model

Direction

Map rule

Model source

XML target

to:

empty: :empty

empty string ("")

blank XML element

omitted: :omitted

omitted in the model

XML element not rendered

nil: :nil

nil value in the model

blank XML element with attribute xsi:nil (<status xsi:nil=true/>)

Default value map for XML element (collection attribute)
attribute :attr, :string, collection: true

xml do
  map_element 'key', to: :attr, value_map: {
    from: { empty: :empty, omitted: :omitted, nil: :nil },
    to: { empty: :empty, omitted: :omitted, nil: :nil }
  }
end
Table 7. Default missing value mapping configuration for collection attributes in XML elements

Direction

Map rule

XML source

Model target

from:

empty: :nil

blank XML element (<status/>)

empty array ([])

omitted: :omitted

absent XML element

omitted from the model

nil: :nil

blank XML element with attribute xsi:nil (<status xsi:nil=true/>)

nil value in the model

Direction

Map rule

Model source

XML target

to:

empty: :empty

empty array ([])

blank XML element

omitted: :omitted

omitted in the model

XML element not rendered

nil: :nil

nil value in the model

blank XML element with attribute xsi:nil (<status xsi:nil=true/>)

Default value map for XML attribute (single attribute)
attribute :attr, :string

xml do
  map_attribute 'attr_name', to: :attr, value_map: {
    from: { empty: :nil, omitted: :omitted },
    to: { empty: :empty, nil: :empty, omitted: :omitted }
  }
end
Table 8. Default missing value mapping configuration for single attributes in XML attributes

Direction

Map rule

XML source

Model target

from: (source only supports empty and omitted)

empty: :nil

blank XML attribute (status="")

nil

omitted: :omitted

absent XML attribute

omitted from the model

Direction

Map rule

Model source

XML target

to: (target only accepts empty and omitted)

empty: :empty

empty string ("")

blank XML attribute (status="")

omitted: :omitted

omitted in the model

XML attribute not rendered

nil: :empty

nil

blank XML attribute (status="")

Default value map for XML attribute (collection attribute)
attribute :attr, :string, collection: true

xml do
  map_attribute 'attr_name', to: :attr, value_map: {
    from: { empty: :empty, omitted: :omitted },
    to: { empty: :empty, nil: :omitted, omitted: :omitted }
  }
end
Table 9. Default missing value mapping configuration for collection attributes in XML attributes

Direction

Map rule

XML source

Model target

from: (source only supports empty and omitted)

empty: :empty

blank XML attribute (status="")

empty array ([])

omitted: :omitted

absent XML attribute

omitted from the model

Direction

Map rule

Model source

XML target

to: (target only accepts empty and omitted)

empty: :empty

empty array ([])

blank XML attribute (status="")

omitted: :omitted

omitted in the model

XML attribute not rendered

nil: :omitted

nil

XML attribute not rendered

Default value map for YAML (single attribute)
attribute :attr, :string

yaml do
  map 'key', to: :attr, value_map: {
    from: { empty: :empty, omitted: :omitted, nil: :nil },
    to: { empty: :empty, omitted: :omitted, nil: :nil }
  }
end
Table 10. Default missing value mapping configuration for single attributes in YAML

Direction

Map rule

XML source

Model target

from:

empty: :empty

empty string in YAML (status: or status: "")

empty string ("")

omitted: :omitted

absent YAML key

omitted from the model

nil: :nil

YAML null

nil

Direction

Map rule

Model source

XML target

to:

empty: :empty

empty string ("")

empty string in YAML (status:)

omitted: :omitted

omitted in the model

YAML key omitted

nil: :nil

nil

YAML null

Note
In order to treat a YAML value like status: '' to nil, the mapping of value_map: { from: { empty: :nil } } can be applied.
Default value map for YAML (collection attribute)
attribute :attr, :string, collection: true

yaml do
  map 'key', to: :attr, value_map: {
    from: { empty: :empty, omitted: :omitted, nil: :nil },
    to: { empty: :empty, omitted: :omitted, nil: :nil }
  }
end
Table 11. Default missing value mapping configuration for collection attributes in YAML

Direction

Map rule

YAML source

Model target

from:

empty: :empty

empty YAML array (status: or status: [])

empty array ([])

omitted: :omitted

absent YAML key

omitted from the model

nil: :nil

YAML null

nil

Direction

Map rule

Model source

YAML target

to:

empty: :empty

empty array ([])

empty YAML array (status: [])

omitted: :omitted

omitted in the model

YAML key omitted

nil: :nil

nil

YAML null

Note
If the YAML key for the collection attribute is omitted, it will be treated as nil or an empty array depending on the initialize_empty setting.
Default value map for JSON (single attribute)
attribute :attr, :string

json do
  map 'key', to: :attr, value_map: {
    from: { empty: :empty, omitted: :omitted, nil: :nil },
    to: { empty: :empty, omitted: :omitted, nil: :nil }
  }
end
Table 12. Default missing value mapping configuration for single attributes in JSON

Direction

Map rule

JSON source

Model target

from:

empty: :empty

empty string in JSON ("status" : "")

empty string ("")

omitted: :omitted

absent JSON key

omitted from the model

nil: :nil

JSON null

nil

Direction

Map rule

Model source

JSON target

to:

empty: :empty

empty string ("")

empty string in JSON ("status" : "")

omitted: :omitted

omitted in the model

JSON key omitted

nil: :nil

nil

JSON null

Default value map for JSON (collection attribute)
attribute :attr, :string, collection: true

json do
  map 'key', to: :attr, value_map: {
    from: { empty: :empty, omitted: :omitted, nil: :nil },
    to: { empty: :empty, omitted: :omitted, nil: :nil }
  }
end
Table 13. Default missing value mapping configuration for collection attributes in JSON

Direction

Map rule

JSON source

Model target

from:

empty: :empty

empty JSON array ("status": [])

empty array ([])

omitted: :omitted

absent JSON key

omitted from the model

nil: :nil

JSON null

nil

Direction

Map rule

Model source

JSON target

to:

empty: :empty

empty array ([])

empty JSON array ("status": [])

omitted: :omitted

omitted in the model

JSON key omitted

nil: :nil

nil

JSON null

Default value map for TOML (single attribute)

TOML does not support the concept of nil and therefore the mapping of from: direction with nil to will not work in TOML.

The nil mapping is only supported in the to: direction (model to TOML).

attribute :attr, :string

toml do
  map 'key', to: :attr, value_map: {
    from: { empty: :empty, omitted: :omitted },
    to: { empty: :empty, omitted: :omitted, nil: :omitted }
  }
end
Table 14. Default missing value mapping configuration for single attributes in TOML

Direction

Map rule

TOML source

Model target

from: (source only supports empty and omitted)

empty: :empty

empty string in TOML ([status] with no value)

empty string ("")

omitted: :omitted

absent TOML key

omitted from the model

Direction

Map rule

Model source

TOML target

to: (source only supports empty and omitted)

empty: :empty

empty string ("")

empty string in TOML ([status] with no value)

omitted: :omitted

omitted in the model

TOML key omitted

nil: :omitted

nil

TOML key omitted

Default value map for TOML (collection attribute)

TOML does not support the concept of nil and therefore the mapping of from: direction with nil to will not work in TOML.

The nil mapping is only supported in the to: direction (model to TOML).

attribute :attr, :string, collection: true

toml do
  map 'key', to: :attr, value_map: {
    from: { empty: :empty, omitted: :omitted },
    to: { empty: :empty, omitted: :omitted, nil: :omitted }
  }
end
Table 15. Default missing value mapping configuration for collection attributes in TOML

Direction

Map rule

TOML source

Model target

from: (source only supports empty and omitted)

empty: :empty

empty TOML array ([status] with no value)

empty array ([])

omitted: :omitted

absent TOML key

omitted from the model

Direction

Map rule

Model source

TOML target

to: (source only supports empty and omitted)

empty: :empty

empty array ([])

empty TOML array ([status] with no value)

omitted: :omitted

omitted in the model

TOML key omitted

nil: :omitted

nil

TOML key omitted

Replacing missing values type mapping with value_map

The value_map option can be defined to meticulously map for each serialization format as follows.

Using value_map with from and to values
class ExampleClass < Lutaml::Model::Serializable
  attribute :status, :string

  xml do
    map_element 'status', to: :status, value_map: {
      from: { empty: :nil, omitted: :omitted, nil: :nil },
      to: { empty: :nil, omitted: :omitted, nil: :nil }
    }
  end

  hsh | json | yaml | toml | key_value do
    map 'status', to: :status, value_map: {
      from: { empty: :nil, omitted: :omitted, nil: :nil },
      to: { empty: :nil, omitted: :omitted, nil: :nil }
    }
  end
end

yaml = <<~YAML
---
status: ''
YAML

ExampleClass.from_yaml(yaml)
# => #<ExampleClass:0x000000011954efb0 @status=nil>

yaml1 = <<~YAML
---
YAML

ExampleClass.from_yaml(yaml1)
# => #<ExampleClass:0x000000011954efb0 @status=uninitialized>

yaml2 = <<~YAML
---
status:
YAML

ExampleClass.from_yaml(yaml2)
# => #<ExampleClass:0x000000011954efb0 @status=nil>

When defining an attribute with collection: true, the attribute will behave as follows:

attribute :status, :string, collection: true

Here’s an example of how you can use the value_map with a collection attribute.

Example 58. Using value_map with a collection attribute
class ExampleClass < Lutaml::Model::Serializable
  attribute :status, :string, collection: true

  xml do
    map_element 'status', to: :status, value_map: {
      from: { empty: :nil, omitted: :omitted, nil: :nil },
      to: { empty: :nil, omitted: :omitted, nil: :nil }
    }
  end

  hsh | json | yaml | key_value do
    map 'status', to: :status, value_map: {
      from: { empty: :nil, omitted: :omitted, nil: :nil },
      to: { empty: :nil, omitted: :omitted, nil: :nil }
    }
  end

  toml do
    map 'status', to: :status, value_map: {
      from: { empty: :nil, omitted: :omitted },
      to: { empty: :nil, omitted: :omitted, nil: :omitted }
    }
  end
end

yaml = <<~YAML
---
status: ['new', 'assigned']
YAML

y = ExampleClass.from_yaml(yaml)
# => #<ExampleClass:0x000000011954efb0 @status=["new", "assigned"]>

Specific overrides of value map (render_* and treat_*)

General

There are times that one may want to simply override handling of selective missing value types rather than re-define the entire value map.

The :render_* and :treat_* options are simple switches that override the default value map provided for the different serialization formats.

Syntax:

{map_command} 'format-key', to: :attribute_name, (1)
  :render_{model-value}: :as_{format-value}, (2)
  # ...
  :treat_{format-value}: :as_{model-value}, (3)
  # ...
  1. The {map_command} is a mapping rule with the actual command depending on the serialization format. The attribute of attribute_name may be a single or a collection value.

  2. The :render_* mapping overrides the default value map for missing value types in model-to-serialization.

  3. The :treat_* mapping overrides the default value map for missing value types in serialization-to-model.

Specifically,

  • The :render_{model-value}: :as_{format-value} options are used to override the default behavior of rendering missing value types into the serialization format.

    {model-value}

    specifies the missing value type in the LutaML Model.

    {format-value}

    specifies the missing value type in the serialization format.

  • The :treat_{format-value}: :as_{model-value} options are used to override the default behavior of importing missing value types into the model.

    {format-value}

    specifies the missing value type in the serialization format.

    {model-value}

    specifies the missing value type in the LutaML Model.

In effect, the default value_map is overriden by the :render_* and :treat_* directives.

Given the default mapping for an XML element, the :render_* and :treat_* options can be used to selectively override behavior.

xml do
  map_element 'key', to: :attr, value_map: {
    from: { empty: :nil, omitted: :omitted, nil: :nil },
    to: { empty: :empty, omitted: :omitted, nil: :nil }
  }
end

By changing to this:

xml do
  map_element 'key', to: :attr,
    render_nil: :as_empty, (1)
    treat_omitted: :as_nil (2)
end
  1. This overrides the to: direction nil: :nil mapping.

  2. This overrides the from: direction omitted: :omitted mapping

The resulting value map would be:

xml do
  map_element 'status', to: :status, value_map: {
    from: { empty: :nil, omitted: :omitted, nil: :empty }, (1)
    to: { empty: :nil, omitted: :nil, nil: :nil }          (2)
  }
end
  1. See that nil: :nil is now nil: :empty.

  2. See that omitted: :omitted is now omitted: :nil

render_nil

General

:render_nil is a specially handled case of the :render_* pattern due to legacy. It is used to override default value map behavior for the nil model value.

render_nil accepts these values:

:as_empty/:as_blank

if the value is nil, render it as an empty string.

:nil

if the value is nil, render it as an element with attribute xsi:nil.

:omit

if the value is nil, omit the element or attribute.

true

(legacy) setting render_nil: true will render the attribute as an empty element if the attribute is nil. This has the same effect as render_nil: :as_empty.

Syntax:

xml do
  map_element 'key_value_model_attribute_name', to: :name_of_attribute, render_nil: {option}
end
hsh | json | yaml | toml do
  map 'key_value_model_attribute_name', to: :name_of_attribute, render_nil: {option}
end
Render nil as true
Example 59. Using the render_nil: true option to render an attribute value of nil as an empty element
class Ceramic < Lutaml::Model::Serializable
  attribute :type, :string
  attribute :glaze, :string

  xml do
    map_element 'type', to: :type, render_nil: true
    map_element 'glaze', to: :glaze
  end

  json do
    map 'type', to: :type, render_nil: true
    map 'glaze', to: :glaze
  end
end
> Ceramic.new.to_json
> # { 'type': null }
> Ceramic.new(type: "Porcelain", glaze: "Clear").to_json
> # { 'type': 'Porcelain', 'glaze': 'Clear' }
> Ceramic.new.to_xml
> # <Ceramic><type></type></Ceramic>
> Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml
> # <Ceramic><type>Porcelain</type><glaze>Clear</glaze></Ceramic>
Example 60. Using the render_nil: true option to render an empty attribute collection of nil as an empty element
class Ceramic < Lutaml::Model::Serializable
  attribute :type, :string
  attribute :glazes, :string, collection: true

  xml do
    map_element 'type', to: :type, render_nil: true
    map_element 'glazes', to: :glazes, render_nil: true
  end

  json do
    map 'type', to: :type, render_nil: true
    map 'glazes', to: :glazes, render_nil: true
  end
end
> Ceramic.new.to_json
> # { 'type': null, 'glazes': [] }
> Ceramic.new(type: "Porcelain", glazes: ["Clear"]).to_json
> # { 'type': 'Porcelain', 'glazes': ['Clear'] }
> Ceramic.new.to_xml
> # <Ceramic><type></type><glazes></glazes></Ceramic>
> Ceramic.new(type: "Porcelain", glazes: ["Clear"]).to_xml
> # <Ceramic><type>Porcelain</type><glazes>Clear</glazes></Ceramic>
Render nil as omit

Using render_nil: :omit with a nil value will omit the key from XML and key-value formats.

class SomeModel < Lutaml::Model::Serializable
  attribute :coll, :string, collection: true

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

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

puts SomeModel.new.coll
# => nil

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

puts SomeModel.new.to_yaml
# =>
# ---
Render nil as nil

Using render_nil: :as_nil with a nil value will create an empty element with xsi:nil attribute in XML and create a key with explicit null value in key-value formats.

Note
TOML does not support this option.
class SomeModel < Lutaml::Model::Serializable
  attribute :coll, :string, collection: true

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

  hsh | json | yaml do
    map 'collection', to: :coll, render_nil: :as_nil
  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
Render nil as blank

Using render_nil: :as_blank | :as_empty will create a blank element in XML and create a key with an explicit empty array in key-value formats.

class SomeModel < Lutaml::Model::Serializable
  attribute :coll, :string, collection: true

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

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

puts SomeModel.new.coll
# => nil

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

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

render_empty

General

:render_empty is a specially handled case of the :render_* pattern due to legacy. It is used to override default value map behavior for the nil model value.

render_empty accepts these values:

:as_empty/:as_blank

if the value is nil, render it as an empty string.

:nil

if the value is nil, render it as an element with attribute xsi:nil.

:omit

if the value is nil, omit the element or attribute.

Syntax:

xml do
  map_element 'key_value_model_attribute_name', to: :name_of_attribute, render_empty: {option}
end
hsh | json | yaml | toml do
  map 'key_value_model_attribute_name', to: :name_of_attribute, render_empty: {option}
end
Render empty as omit

Using render_empty: :omit with an empty value or empty collection will omit the key from XML and key-value formats.

class SomeModel < Lutaml::Model::Serializable
  attribute :coll, :string, collection: true

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

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

puts SomeModel.new(coll: []).coll
# => []

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

puts SomeModel.new.to_yaml
# =>
# ---
Render empty as nil

Using render_empty: :as_nil will create an empty element with the xsi:nil attribute in XML, and create a key with explicit null value in key-value formats.

Note
TOML does not support this option.
class SomeModel < Lutaml::Model::Serializable
  attribute :coll, :string, collection: true

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

  hsh | json | yaml do
    map 'collection', to: :coll, render_empty: :as_nil
  end
end

puts SomeModel.new(coll: []).coll
# => []

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

puts SomeModel.new.to_yaml
# =>
# ---
# coll: null
Render empty as blank/empty

Using render_empty: :as_blank or render_empty: :as_empty will create a blank element in XML and create a key with an explicit empty array in key-value formats.

class SomeModel < Lutaml::Model::Serializable
  attribute :coll, :string, collection: true

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

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

puts SomeModel.new(coll: []).coll
# => []

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

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

Importing data models

General

Lutaml::Model provides a way to import data models defined from various formats into the LutaML data modeling system.

Data model languages supported are:

The following figure illustrates the process of importing an XML Schema model to create LutaML core models. Once the LutaML core models are created, they can be used to parse and generate XML documents according to the imported XML Schema model.

Today, the LutaML core models are written into Ruby files, which can be used to parse and generate XML documents according to the imported XML Schema. This is to be changed so that the LutaML core models are directly loaded and interpreted.

Importing an XML Schema model to create LutaML core models
╔════════════════════════════╗                        ╔═══════════════════════╗
║    Serialization Models    ║                        ║       Core Model      ║
╚════════════════════════════╝                        ╚═══════════════════════╝

╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮                        ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
┆  XML Schema (XSD/RNG/RNC)  ┆                        ┆          Model        ┆
┆              │             ┆   ┌────────────────┐   ┆            │          ┆
┆       ┌──────┴──────┐      ┆   │                │   ┆   ┌────────┴──┐       ┆
┆       │             │      ┆   │     Model      │   ┆   │           │       ┆
┆    Models      Value Types ┆──►│   Importing    │──►┆ Models   Value Types  ┆
┆       │             │      ┆   │                │   ┆   │           │       ┆
┆       │             │      ┆   └────────────────┘   ┆   │           │       ┆
┆  ┌────┴────┐      ┌─┴─┐    ┆           │            ┆   │    ┌──────┴──┐    ┆
┆  │         │      │   │    ┆           │            ┆   │    │         │    ┆
┆ Element  Value  xs:string  ┆           │            ┆   │   String  Integer ┆
┆ Attribute Type  xs:date    ┆           │            ┆   │   Date    Float   ┆
┆ Union  Complex  xs:boolean ┆           │            ┆   │   Time    Boolean ┆
┆ Sequence Choice xs:anyURI  ┆           │            ┆   │                   ┆
╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯           │            ┆   └──────┐            ┆
                                         │            ┆          │            ┆
                                         │            ┆     Contains          ┆
                                         │            ┆     more Models       ┆
                                         │            ┆     (recursive)       ┆
                                         │            ┆                       ┆
                                         │            ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯
                                         │            ┌────────────────┐
                                         │            │                │
                                         │            │     Model      │
                                         └──────────► │ Transformation │
                                                      │       &        │
                                                      │ Mapping Rules  │
                                                      │                │
                                                      └────────────────┘

XML Schema (XSD)

W3C XSD is a schema language designed to define the structure of XML documents, alongside other XML schema languages like DTD, RELAX NG, and Schematron.

Lutaml::Model supports the import of XSD schema files to define information models that can be used to parse and generate XML documents.

Specifically, the Lutaml::Model::Schema#from_xml method loads XML Schema files (XSD, .xsd) and generates Ruby files (.rb) that inherit from Lutaml::Model::Serializable that are saved to disk.

Syntax:

Lutaml::Model::Schema.from_xml(
  xsd_schema, (1)
  options: options (2)
)
  1. The xsd_schema is the XML Schema string to be converted to model files.

  2. The options hash is an optional argument.

    options

    Optional hash containing potentially the following key-values.

    output_dir

    The directory where the model files will be saved. If not provided, a default directory named lutaml_models_<timestamp> is created.

    "path/to/directory"
    create_files

    A boolean argument (false by default) to create files directly in the specified directory as defined by the output_dir option.

    create_files: (true | false)
    load_classes

    A boolean argument (false by default) to load generated classes before returning them.

    load_classes: (true | false)
    namespace

    The namespace of the schema. This will be added in the Lutaml::Model::Serializable file’s xml do block.

    prefix

    The prefix of the namespace provided in the namespace option.

    example-prefix
    location

    The URL or path of the directory containing all the files of the schema. For more information, refer to the XML Schema specification.

    "http://example.com/example.xsd"
    "path/to/schema/directory"
Note
If both create_files and load_classes are provided, the create_files argument will take priority and generate files without loading them!

The generated LutaML models consists of two different kind of Ruby classes depending on the XSD schema:

XSD "SimpleTypes"

converted into classes that inherit from Lutaml::Model::Type::Value, which define the data types with restrictions and other validations of these values.

XSD "ComplexTypes"

converted into classes that inherit from Lutaml::Model::Serializable that model according to the defined structure.

Lutaml::Model uses the lutaml-xsd gem to automatically resolve the include and import elements, enabling Lutaml-Model to generate the corresponding model files.

This auto-resolving feature allows seamless integration of these files into your models without the need for manual resolution of includes and imports.

Example 61. Using Lutaml::Model::Schema#from_xml to convert an XML Schema to model files
xsd_schema = <<~XSD
  <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    /* your schema here */
  </xs:schema>
XSD
options = {
  # These are all optional:
  output_dir: 'path/to/directory',
  namespace: 'http://example.com/namespace',
  prefix: "example-prefix",
  location: "http://example.com/example.xsd",
  # or
  # location: "path/to/schema/directory"
  create_files: true, # Default: false
  # OR
  load_classes: true, # Default: false
}

# generates the files in the output_dir | default_dir
Lutaml::Model::Schema.from_xml(xsd_schema, options: options)

You could also directly load the generated Ruby files into your application by requiring them.

Example 62. Using the generated Ruby files in your application
Lutaml::Model::Schema.from_xml(xsd_schema, options: {output_dir: 'path/to/directory'})
require_relative 'path/to/directory/*.rb'

Validation

General

Lutaml::Model provides a way to validate data models using the validate and validate! methods.

  • The validate method sets an errors array in the model instance that contains all the validation errors. This method is used for checking the validity of the model silently.

  • The validate! method raises a Lutaml::Model::ValidationError that contains all the validation errors. This method is used for forceful validation of the model through raising an error.

Lutaml::Model supports the following validation methods:

  • collection:: Validates collection size range.

  • values:: Validates the value of an attribute from a set of fixed values.

  • choice :: Validates that attribute specified within defined range

The following class will validate the degree_settings attribute 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 Kiln < Lutaml::Model::Serializable
  attribute :name, :string
  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

kiln = Kiln.new(name: "Kiln", degree_settings: [100, 200, 300], description: "one", prefix: "Ben")
kiln.validate
# => []

kiln = Kiln.new(name: "Kiln", degree_settings: [], description: "four", prefix: "Ben", nick_name: "Smith")
kiln.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 = kiln.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>
#   ]

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

kiln = Kiln.new(name: "Kiln", type: "glass", degree_settings: [100, 200, 1400])
kiln.validate
# => [#<Lutaml::Model::Error: Degree settings for glass must be less than 1300>]

Liquid template access

Warning
The Liquid template feature is optional. To enable it, please explicitly require the liquid gem.

The Liquid template language is an open-source template language developed by Shopify and written in Ruby.

Lutaml::Model::Serializable objects can be safely accessed within Liquid templates through a to_liquid method that converts the objects into Liquid::Drop instances.

  • All attributes are accessible in the Liquid template by their names.

  • Nested attributes are also converted into Liquid::Drop objects so inner attributes can be accessed using the Liquid dot notation.

Note
Every Lutaml::Model::Serializable class extends the Liquefiable module which generates a corresponding Liquid::Drop class.
Note
Methods defined in the Lutaml::Model::Serializable class are not accessible in the Liquid template.
Example 63. Using to_liquid to convert model instances into corresponding Liquid drop instances
class Ceramic < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :temperature, :integer
end

ceramic = Ceramic.new({ name: "Porcelain Vase", temperature: 1200 })
ceramic_drop = ceramic.to_liquid
# Ceramic::CeramicDrop

puts ceramic_drop.name
# "Porcelain Vase"
puts ceramic_drop.temperature
# 1200
Example 64. Accessing LutaML::Model objects within a Liquid template
class Ceramic < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :temperature, :integer
end

class CeramicCollection < Lutaml::Model::Serializable
  attribute :ceramics, Ceramic, collection: true
end

sample.yml:

---
ceramics:
- name: Porcelain Vase
  temperature: 1200
- name: Earthenware Pot
  temperature: 950
- name: Stoneware Jug
  temperature: 1200

template.liquid:

{% for ceramic in ceramic_collection.ceramics %}
* Name: "{{ ceramic.name }}"
** Temperature: {{ ceramic.temperature }}
{%- endfor %}
# Load the Lutaml::Model collection
ceramic_collection = CeramicCollection.from_yaml(File.read("sample.yml"))

# Load the Liquid template
template = Liquid::Template.parse(File.read("template.liquid"))

# Pass the Lutaml::Model collection to the Liquid template and render
output = template.render("ceramic_collection" => ceramic_collection)
puts output
# >
# * Name: "Porcelain Vase"
# ** Temperature: 1200
# * Name: "Earthenware Pot"
# ** Temperature: 950
# * Name: "Stoneware Jug"
# ** Temperature: 1200
Example 65. Accessing nested LutaML::Model objects within nested Liquid templates
class Glaze < Lutaml::Model::Serializable
  attribute :color, :string
  attribute :opacity, :string
end

class CeramicWork < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :glaze, Glaze
end

class CeramicCollection < Lutaml::Model::Serializable
  attribute :ceramics, Ceramic, collection: true
end

ceramic_work = CeramicWork.new({
  name: "Celadon Bowl",
  glaze: Glaze.new({
    color: "Jade Green",
    opacity: "Translucent"
  })
})
ceramic_work_drop = ceramic_work.to_liquid
# CeramicWork::CeramicWorkDrop

puts ceramic_work_drop.name
# "Celadon Bowl"
puts ceramic_work_drop.glaze.color
# "Jade Green"
puts ceramic_work_drop.glaze.opacity
# "Translucent"

ceramics.yml:

---
ceramics:
- name: Celadon Bowl
  glaze:
    color: Jade Green
    opacity: Translucent
- name: Earthenware Pot
  glaze:
    color: Rust Red
    opacity: Opaque
- name: Stoneware Jug
  glaze:
    color: Cobalt Blue
    opacity: Transparent

templates/_ceramics.liquid:

{% for ceramic in ceramic_collection.ceramics %}
{% render 'ceramic' ceramic: ceramic %}
{%- endfor %}
Note
render is a Liquid tag that renders a partial template, by default Liquid uses the pattern _%s.liquid to find the partial template. Here ceramic refers to the file at templates/_ceramic.liquid.

templates/_ceramic.liquid:

* Name: "{{ ceramic.name }}"
** Temperature: {{ ceramic.temperature }}
{%- if ceramic.glaze %}
** Glaze (color): {{ ceramic.glaze.color }}
** Glaze (opacity): {{ ceramic.glaze.opacity }}
{%- endif %}
require 'liquid'

# Create a Liquid template object that supports dynamic loading
template = Liquid::Template.new

# Link the Liquid template object to a "local file system" (directory)
file_system = Liquid::LocalFileSystem.new('templates/')
template.registers[:file_system] = file_system

# Load the partial template, this is necessary.
# This will also allow Liquid to load any inner partials from the file system
# dynamically (see `file_system.pattern` to see what it loads)
template.parse(file_system.read_template_file('ceramics'))

# Read the lutaml-model collection
ceramic_collection = CeramicCollection.from_yaml(File.read("ceramics.yml"))

# Render the template with the collection
output = template.render("ceramic_collection" => ceramic_collection)
puts output
# >
# * Name: "Celadon Bowl"
# ** Temperature: 1200
# ** Glaze (color): Jade Green
# ** Glaze (finish): Translucent
# * Name: "Earthenware Pot"
# ** Temperature: 950
# ** Glaze (color): Rust Red
# ** Glaze (finish): Opaque
# * Name: "Stoneware Jug"
# ** Temperature: 1200
# ** Glaze (color): Cobalt Blue
# ** Glaze (finish): Transparent

Adapters

General

Lutaml::Model uses an adapter pattern to support multiple libraries for each serialization format.

Lutaml::Model supports the following serialization formats:

You will need to specify the configuration for the adapter you want to use. The easiest way is to copy and paste the following configuration into your code.

The configuration is as follows:

require 'lutaml/model'
require 'lutaml/model/xml_adapter/nokogiri_adapter'
require 'lutaml/model/json_adapter/standard_json_adapter'
require 'lutaml/model/toml_adapter/toml_rb_adapter'
require 'lutaml/model/yaml_adapter/standard_yaml_adapter'

Lutaml::Model::Config.configure do |config|
  config.xml_adapter = Lutaml::Model::XmlAdapter::NokogiriAdapter
  config.hash_adapter = Lutaml::Model::YamlAdapter::StandardHashAdapter
  config.yaml_adapter = Lutaml::Model::YamlAdapter::StandardYamlAdapter
  config.json_adapter = Lutaml::Model::JsonAdapter::StandardJsonAdapter
  config.toml_adapter = Lutaml::Model::TomlAdapter::TomlRbAdapter
end

You can also provide the adapter type by using symbols like

require 'lutaml/model'

Lutaml::Model::Config.configure do |config|
  config.xml_adapter_type = :nokogiri # can be one of [:nokogiri, :ox, :oga]
  config.hash_adapter_type = :standard_hash
  config.yaml_adapter_type = :standard_yaml
  config.json_adapter_type = :standard_json # can be one of [:standard_json, :multi_json]
  config.toml_adapter_type = :toml_rb # can be one of [:toml_rb, :tomlib]
end
Note
By default yaml_adapter_type and json_adapter_type are set to :standard_yaml and :standard_json respectively.

XML

Lutaml::Model supports the following XML adapters:

Nokogiri

(default) Popular libxml based XML parser for Ruby. Requires native extensions (i.e. compiled C code). Requires the nokogiri gem.

Oga

(optional) Pure Ruby XML parser. Does not require native extensions and is suitable for Opal (Ruby on JavaScript). Requires the oga gem.

Ox

(optional) Fast XML parser and object serializer for Ruby, implemented partially in C. Requires native extensions (i.e. compiled C code). Requires the ox gem.

Using the Nokogiri XML adapter
require 'lutaml/model'

Lutaml::Model::Config.configure do |config|
  require 'lutaml/model/xml_adapter/nokogiri_adapter'
  config.xml_adapter = Lutaml::Model::XmlAdapter::NokogiriAdapter
end
Using the Oga XML adapter
require 'lutaml/model'

Lutaml::Model::Config.configure do |config|
  require 'lutaml/model/xml_adapter/oga_adapter'
  config.xml_adapter = Lutaml::Model::XmlAdapter::OgaAdapter
end
Using the Ox XML adapter
require 'lutaml/model'

Lutaml::Model::Config.configure do |config|
  require 'lutaml/model/xml_adapter/ox_adapter'
  config.xml_adapter = Lutaml::Model::XmlAdapter::OxAdapter
end

YAML

Lutaml::Model supports only one YAML adapter.

YAML

(default) The Psych YAML parser and emitter for Ruby. Included in the Ruby standard library.

Using the YAML adapter
require 'lutaml/model'

Lutaml::Model::Config.configure do |config|
  require 'lutaml/model/yaml_adapter/standard_yaml_adapter'
  config.yaml_adapter = Lutaml::Model::YamlAdapter::StandardYamlAdapter
end

JSON

Lutaml::Model supports the following JSON adapters:

JSON

(default) The standard JSON library for Ruby. Included in the Ruby standard library.

MultiJson

(optional) A gem that provides a common interface to multiple JSON libraries. Requires the multi_json gem.

Using the JSON adapter
require 'lutaml/model'

Lutaml::Model::Config.configure do |config|
  require 'lutaml/model/json_adapter/standard_json_adapter'
  config.json_adapter = Lutaml::Model::JsonAdapter::StandardJsonAdapter
end
Using the MultiJson adapter
require 'lutaml/model'

Lutaml::Model::Config.configure do |config|
  require 'lutaml/model/json_adapter/multi_json_adapter'
  config.json_adapter = Lutaml::Model::JsonAdapter::MultiJsonAdapter
end

TOML

Lutaml::Model supports the following TOML adapters:

Toml-rb

(default) A TOML parser and serializer for Ruby that is compatible with the TOML v1.0.0 specification. Requires the toml-rb gem.

Tomlib

(optional) Toml-rb fork that is compatible with the TOML v1.0.0 specification, but with additional features. Requires the tomlib gem.

Using the Toml-rb adapter
require 'lutaml/model'

Lutaml::Model::Config.configure do |config|
  require 'lutaml/model/toml_adapter/toml_rb_adapter'
  config.toml_adapter = Lutaml::Model::TomlAdapter::TomlRbAdapter
end
Using the Tomlib adapter
require 'lutaml/model'

Lutaml::Model::Config.configure do |config|
  config.toml_adapter = Lutaml::Model::TomlAdapter::TomlibAdapter
  require 'lutaml/model/toml_adapter/tomlib_adapter'
end

Custom Adapters

Lutaml::Model provides a flexible system for creating custom adapters to handle different data formats. For more information See Custom Adapters Guide

Comparison with Shale

Lutaml::Model is a serialization library that is similar to Shale, but with some differences in implementation.

Feature Lutaml::Model Shale Notes

Data model definition

2 types:

  • Inherit from Shale::Mapper

  • Custom model class

Value types

Lutaml::Model::Type includes: Integer, String, Float, Boolean, Date, DateTime, Time, Decimal, Hash.

Shale::Type includes: Integer, String, Float, Boolean, Date, Time.

Lutaml::Model supports additional value types Decimal, DateTime and Hash.

Configuration

Lutaml::Model::Config

Shale.{type}_adapter

Lutaml::Model uses a configuration block to set the serialization adapters.

Custom serialization methods

:with, on individual attributes

:using, on entire object/document

Lutaml::Model uses the :with keyword for custom serialization methods.

Serialization formats

XML, YAML, JSON, TOML

XML, YAML, JSON, TOML, CSV

Lutaml::Model does not support CSV.

Validation

Supports collection range, fixed values, and custom validation

Requires implementation

Adapter support

XML (Nokogiri, Ox, Oga), YAML, JSON (JSON, MultiJson), TOML (Toml-rb, Tomlib)

XML (Nokogiri, Ox), YAML, JSON (JSON, MultiJson), TOML (Toml-rb, Tomlib), CSV

Lutaml::Model does not support CSV.

XML features

Yes. Supports <root xmlns='http://example.com'> through the namespace option without prefix.

No. Only supports <root xmlns:prefix='http://example.com'>.

XML mixed content support

Yes. Supports the following kind of XML through mixed content support.

<description>My name is
<bold>John Doe</bold>,
and I'm <i>28</i>
years old</description>

No. Shale’s map_content only supports the first text node.

XML namespace inheritance

Yes. Supports the inherit option to inherit the namespace from the root element.

No.

Support for xsi:schemaLocation

Yes. Automatically supports the xsi:schemaLocation attribute for every element.

Requires manual specification on every XML element that uses it.

Compiling XML Schema to Lutaml::Model::Serializable classes

  1. ComplexTypes are compiled to Lutaml::Model::Serializable classes containing the attributes.

  2. SimpleTypes are compiled to Lutaml::Model::Type::Value classes to support XML Schema level validations.

Yes, Provides only an array of the classes and doesn’t support simple types with restrictions and/or other validations.

Attribute features

Attribute delegation

:delegate option to delegate attribute mappings to a model.

:receiver option to delegate attribute mappings to a model.

Enumerations

Yes. Supports enumerations as value types through the values: option.

No.

Lutaml::Model supports enumerations as value types.

Attribute extraction

Yes. Supports attribute extraction from key-value data models.

No.

Lutaml::Model supports attribute extraction from key-value data models.

Migration steps from Shale

The following sections provide a guide for migrating from Shale to Lutaml::Model.

Step 1: Replace inheritance class

Lutaml::Model uses Lutaml::Model::Serializable as the base inheritance class.

class Example < Lutaml::Model::Serializable
  # ...
end
Note

Lutaml::Model also supports an inclusion method as in the following example, which is not supported by Shale. This is useful for cases where you want to include the serialization methods in a class that already inherits from another class.

class Example
  include Lutaml::Model::Serialize
  # ...
end

Shale uses Shale::Mapper as the base inheritance class.

class Example < Shale::Mapper
  # ...
end

Actions:

  • Replace mentions of Shale::Mapper with Lutaml::Model::Serializable.

  • Potentially replace inheritance with inclusion for suitable cases.

Step 2: Replace value type definitions

Value types in Lutaml::Model are under the Lutaml::Model::Type module, or use the LutaML type symbols.

class Example < Lutaml::Model::Serializable
  attribute :length, :integer
  attribute :description, :string
end
Note

Lutaml::Model supports specifying predefined value types as strings or symbols, which is not supported by Shale.

class Example < Lutaml::Model::Serializable
  attribute :length, Lutaml::Model::Type::Integer
  attribute :description, "String"
end

Value types in Shale are under the Shale::Type module.

class Example < Shale::Mapper
  attribute :length, Shale::Type::Integer
  attribute :description, Shale::Type::String
end

Action:

  • Replace mentions of Shale::Type with Lutaml::Model::Type.

  • Potentially replace value type definitions with strings or symbols.

Step 3: Configure serialization adapters

Lutaml::Model uses a configuration block to set the serialization adapters.

require 'lutaml/model/xml_adapter/nokogiri_adapter'
Lutaml::Model::Config.configure do |config|
  config.xml_adapter = Lutaml::Model::XmlAdapter::NokogiriAdapter
end

The equivalent for Shale is this:

require 'shale/adapter/nokogiri'
Shale.xml_adapter = Shale::Adapter::Nokogiri

Here are places that this code may reside at:

  • If your code is a standalone Ruby script, this code will be present in your code.

  • If your code is organized in a Ruby gem, this code will be specified somewhere referenced by lib/your_gem_name.rb.

  • If your code contains tests or specs, they will be in the test setup file, e.g. RSpec spec/spec_helper.rb.

Actions:

  • Replace the Shale configuration block with the Lutaml::Model::Config configuration block.

  • Replace the Shale adapter with the Lutaml::Model adapter.

Step 4: Rewrite custom serialization methods

There is an implementation difference between Lutaml::Model and Shale for custom serialization methods.

Custom serialization methods in Lutaml::Model map to individual attributes.

For custom serialization methods, Lutaml::Model uses the :with keyword instead of the :using keyword used by Shale.

class Example < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :size, :integer
  attribute :color, :string
  attribute :description, :string

  json do
    map "name", to: :name, with: { to: :name_to_json, from: :name_from_json }
    map "size", to: :size
    map "color", to: :color,
                 with: { to: :color_to_json, from: :color_from_json }
    map "description", to: :description,
                       with: { to: :description_to_json, from: :description_from_json }
  end

  xml do
    root "CustomSerialization"
    map_element "Name", to: :name,
                        with: { to: :name_to_xml, from: :name_from_xml }
    map_attribute "Size", to: :size
    map_element "Color", to: :color,
                         with: { to: :color_to_xml, from: :color_from_xml }
    map_content to: :description,
                with: { to: :description_to_xml,
                        from: :description_from_xml }
  end

  def name_to_json(model, doc)
    doc["name"] = "JSON Masterpiece: #{model.name}"
  end

  def name_from_json(model, value)
    model.name = value.sub(/^JSON Masterpiece: /, "")
  end

  def color_to_json(model, doc)
    doc["color"] = model.color.upcase
  end

  def color_from_json(model, value)
    model.color = value.downcase
  end

  def description_to_json(model, doc)
    doc["description"] = "JSON Description: #{model.description}"
  end

  def description_from_json(model, value)
    model.description = value.sub(/^JSON Description: /, "")
  end

  def name_to_xml(model, parent, doc)
    el = doc.create_element("Name")
    doc.add_text(el, "XML Masterpiece: #{model.name}")
    doc.add_element(parent, el)
  end

  def name_from_xml(model, value)
    model.name = value.sub(/^XML Masterpiece: /, "")
  end

  def color_to_xml(model, parent, doc)
    color_element = doc.create_element("Color")
    doc.add_text(color_element, model.color.upcase)
    doc.add_element(parent, color_element)
  end

  def color_from_xml(model, value)
    model.color = value.downcase
  end

  def description_to_xml(model, parent, doc)
    doc.add_text(parent, "XML Description: #{model.description}")
  end

  def description_from_xml(model, value)
    model.description = value.join.strip.sub(/^XML Description: /, "")
  end
end

Custom serialization methods in Shale do not map to specific attributes, but allow the user to specify where the data goes.

class Example < Shale::Mapper
  attribute :name, Shale::Type::String
  attribute :size, Shale::Type::Integer
  attribute :color, Shale::Type::String
  attribute :description, Shale::Type::String

  json do
    map "name", using: { from: :name_from_json, to: :name_to_json }
    map "size", to: :size
    map "color", using: { from: :color_from_json, to: :color_to_json }
    map "description", to: :description, using: { from: :description_from_json, to: :description_to_json }
  end

  xml do
    root "CustomSerialization"
    map_element "Name", using: { from: :name_from_xml, to: :name_to_xml }
    map_attribute "Size", to: :size
    map_element "Color", using: { from: :color_from_xml, to: :color_to_xml }
    map_content to: :description, using: { from: :description_from_xml, to: :description_to_xml }
  end

  def name_to_json(model, doc)
    doc['name'] = "JSON Masterpiece: #{model.name}"
  end

  def name_from_json(model, value)
    model.name = value.sub(/^JSON Masterpiece: /, "")
  end

  def color_to_json(model, doc)
    doc['color'] = model.color.upcase
  end

  def color_from_json(model, doc)
    model.color = doc['color'].downcase
  end

  def description_to_json(model, doc)
    doc['description'] = "JSON Description: #{model.description}"
  end

  def description_from_json(model, doc)
    model.description = doc['description'].sub(/^JSON Description: /, "")
  end

  def name_from_xml(model, node)
    model.name = node.text.sub(/^XML Masterpiece: /, "")
  end

  def name_to_xml(model, parent, doc)
    name_element = doc.create_element('Name')
    doc.add_text(name_element, model.street.to_s)
    doc.add_element(parent, name_element)
  end
end
Note
There are cases where the Shale implementation of custom methods work differently from the Lutaml::Model implementation. In these cases, you will need to adjust the custom methods accordingly.

Actions:

  • Replace the using keyword with the with keyword.

  • Adjust the custom methods.

About LutaML

The name "LutaML" is pronounced as "Looh-tah-mel".

The name "LutaML" comes from the Latin word for clay, "Lutum", and "ML" for "Markup Language". Just as clay can be molded and modeled into beautiful and practical end products, the Lutaml::Model gem is used for data modeling, allowing you to shape and structure your data into useful forms.

This project is licensed under the BSD 2-clause License. See the LICENSE.md file for details.

Copyright Ribose.

About

LutaML Model is an information modeler. Supports declarative definitions of serializations and transforms for models.

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

No packages published

Languages