Serialization: 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 1. 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 2. 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"}
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 3. 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::Xml::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.

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::Xml::NokogiriElement:0x0000000107656ed8
#   @attributes={},
#   @children=
#   [#<Lutaml::Model::Xml::NokogiriElement:0x0000000107656cd0 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n    ">,
#   #<Lutaml::Model::Xml::NokogiriElement:0x00000001076569b0
#     @attributes={},
#     @children=
#     [#<Lutaml::Model::Xml::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::Xml::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::Xml::OxElement:0x0000000107584f78
# @attributes={},
# @children=
#   [#<Lutaml::Model::Xml::OxElement:0x0000000107584e60
#     @attributes={},
#     @children=[#<Lutaml::Model::Xml::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::Xml::Oga::Element:0x0000000107314158
# @attributes={},
# @children=
#   [#<Lutaml::Model::Xml::Oga::Element:0x0000000107314090 @attributes={}, @children=[], @default_namespace=nil, @name="text", @namespace_prefix=nil, @text="\n    ">,
#   #<Lutaml::Model::Xml::Oga::Element:0x000000010730fe78
#     @attributes={},
#     @children=[#<Lutaml::Model::Xml::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::Xml::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 4. 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}

Only One Custom Method

Only one custom method can be added for the serialization or deserialization of an attribute.

Syntax:

xml do
  map_element 'element_name', to: :name_of_element, with: {
    to: :method_name_to_serialize # only 'to' is implemented
  }
  map_element 'element_name', to: :name_of_element, with: {
    from: :method_name_to_deserialize # only 'from' is implemented
  }
end

hsh | json | yaml | toml do
  map 'attribute_name', to: :name_of_attribute, with: {
    to: :method_name_to_serialize # only 'to' is implemented
  }
  map 'attribute_name', to: :name_of_attribute, with: {
    from: :method_name_to_deserialize # only 'from' is implemented
  }
end

This is only applicable if the to: :name_of_element (in xml mapping) or to: :name_of_attribute (in key_value mapping) option is set. If it is not set, then both custom methods must be provided.

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

  xml do
    map_element "Name", to: :name, with: { to: :name_to_xml }
    map_attribute "Size", to: :size
  end

  json do
    map 'name', to: :name, with: { from: :name_from_json }
    map 'size', to: :size
  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_json(model, value)
    model.name = value.sub(/^Masterpiece: /, '')
  end
end
<CustomCeramic Size="15">
  <Name>Vase</Name>
</CustomCeramic>
> CustomCeramic.from_xml(xml)
> #<CustomCeramic:0x0000000108d0e1f8
   @element_order=["text", "Name", "text", "Size", "text"],
   @name="Vase",
   @ordered=nil,
   @size=15>
> puts CustomCeramic.new(name: "Vase", size: 15).to_xml
# <CustomCeramic Size="15">
#   <Name>XML Masterpiece: Vase</Name>
# </CustomCeramic>
{
  "name": "Masterpiece: Vase",
  "size": 12
}
> CustomCeramic.from_json(json)
> #<CustomCeramic:0x0000000104ac7240 @name="Vase", @size=12>
> CustomCeramic.new(name: "Vase", size: 12).to_json
> # {"name":"Vase","size":12}