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