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
endWhen serializing, the first element in the array of mapped names is always used as the output name.
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
element "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
endFor 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
enddelegate option to map attributes to nested objectsThe 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 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
}
endwith: key to define custom serialization methods for XMLThe 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
endWhen 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
hsh | json | yaml | toml do
map 'attribute_name', to: :name_of_attribute, with: {
to: :method_name_to_serialize,
from: :method_name_to_deserialize
}
endwith: key to define custom serialization methodsThe 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
}
endThis 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}Attribute-value dispatch on a shared element name
When several distinct attributes share one wire element, selected by a sibling attribute’s value, declare the discriminator key with when_attribute: and map each value to its attribute:
class Requirement < Lutaml::Model::Serializable
attribute :guidance, Component, collection: true
attribute :purpose, Component, collection: true
xml do
element "requirement"
map_element "component", when_attribute: "type",
to: { "guidance" => :guidance,
"purpose" => :purpose }
end
endThat one declaration expands, at DSL time, to one rule per value — identical to writing each rule by hand:
xml do
element "requirement"
map_element "component", to: :guidance,
when_attribute: { "type" => "guidance" }
map_element "component", to: :purpose,
when_attribute: { "type" => "purpose" }
endUse the per-rule form when a partition is partial (only some values claimed) or a rule needs several pairs — multiple pairs mean AND.
Each rule hydrates only from the <component> occurrences whose type attribute carries the expected value, in document order; uncovered values are ignored by all discriminator rules. On serialization the discriminator pair is re-emitted on the element (unless the value’s own mapping already writes that attribute).
Co-occurrence semantics: several when_attribute rules may share the element name; each target attribute fills independently, so declare each as collection: true when the element repeats. A rule without when_attribute that shares the name only claims occurrences no discriminator rule claimed, so every occurrence is captured exactly once.
On the key-value formats the same coexistence applies to when_attribute rules sharing one key, regardless of declaration order. A rule without when_attribute is an unconditional claim on the bare slot: declaring one replaces prior bare rules on that key — including a parent class’s rule the subclass remaps to a different attribute — while when_attribute partitions on the key survive. A partition rule replaces only a prior partition with the same target; otherwise it accumulates.
By default an occurrence no rule claims — an uncovered discriminator value, or a missing discriminator attribute — is silently dropped. Declare unmatched: :raise on a discriminator rule to fail the parse instead: Lutaml::Model::UnknownDiscriminatorError names the element and the unclaimed discriminator values. A plain rule sharing the name claims everything left, so the strict policy never fires when one is present.
xml do
element "requirement"
map_element "component", when_attribute: "type",
to: { "guidance" => :guidance },
unmatched: :raise
endwhen_attribute and polymorphic: are two different dispatch axes over the same discriminator idea — pick by what the discriminator selects:
-
polymorphic: { attribute:, class_map: }— the value selects the class each occurrence hydrates into; ONE attribute holds every occurrence. See the polymorphic models tutorial. -
when_attribute:— the value selects which attribute claims the occurrence; the declared type stays fixed.
They compose: a rule may partition with when_attribute while its attribute dispatches classes with polymorphic.
Under an ordered mapping, serialization preserves the original interleaving: element_order records each occurrence’s attributes, and the ordered walk routes every occurrence back to the rule whose discriminator it satisfies.
xml do
element "requirement"
ordered
map_element "component", when_attribute: "type",
to: { "guidance" => :guidance,
"purpose" => :purpose }
endGiven <component type="guidance">, <component type="purpose">, <component type="guidance"> in the source, to_xml re-emits exactly that guidance, purpose, guidance sequence.
Key-value formats
The same discriminator works for JSON, YAML, TOML and Hash: the wire name is an object key, and the discriminator is a sibling key’s value inside each item.
class KvRequirement < Lutaml::Model::Serializable
attribute :guidance, Component, collection: true
attribute :purpose, Component, collection: true
json do
map "component", when_attribute: "type",
to: { "guidance" => :guidance, "purpose" => :purpose }
end
end{ "component": [
{ "type": "guidance", "text": "g1" },
{ "type": "purpose", "text": "p1" }
] }guidance holds g1 and purpose holds p1. A bare object instead of an array counts as one occurrence. Every semantics from the XML side carries over: plain rules on the same key capture only unclaimed items, unmatched: :raise fails the parse on an item no rule claims, and string or symbol keys both match.
On serialization the rules merge back under the one key, each item stamped with its rule’s discriminator pair (unless the item’s own mapping already writes that key):
{ "component": [
{ "type": "guidance", "text": "g1" },
{ "type": "purpose", "text": "p1" }
] }Items are grouped by rule in mapping-declaration order — object documents carry no interleaving to preserve. when_attribute cannot be combined with with: or delegate: in key-value mappings.