General

Key-value data models share a similar structure where data is stored as key-value pairs.

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

Key-value data models supported are identified by their short name:

hsh

Hash (Ruby Hash class)

json

JSON

yaml

YAML

toml

TOML

key_value

A way to configure key-value mappings for all supported key-value data models.

Mapping

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

Syntax:

{key_value_type_short} do (1)
  map 'key_value_model_attribute_name', to: :name_of_attribute
end
1 key_value_type_short is the key-value data model’s short name.
Example 1. Creating a key-value data model mapping for only the JSON format
json do
  map :color, to: :color
  map :desc, to: :description
end
Example 2. Creating a key-value data model mapping for all key-value formats
key_value do
  map :color, to: :color
  map :desc, to: :description
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 3. 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 # or any other key-value data model
    map :color, to: :color
    map :glz, to: :glaze
    map :desc, to: :description
  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.

Example 4. 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.

The corresponding method for XML is at [xml-map-all].
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.

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

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

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 9. 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 10. 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 11. 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.