- General
- XML elements and XML types
- XML elements and XML types (for Lutaml::Model)
- General
- Defining element name (
element) - (DEPRECATED) Root method with options
- Declaring an XML type (
elementomitted) - Mixed content elements (
mixed_contentmethod) - (DEPRECATED) Mixed content declaration (
rootmethod withmixed:) - Ordered content (
orderedmethod) - (DEPRECATED) Ordered content declaration (
rootmethod withordered:)
- XML elements and XML types (for Lutaml::Model)
- XML content mapping
General
General
XML is a widely used structured serialization format standardized by the W3C.
At a high level, XML defines the following primitives:
-
XML element (
<element-name>content</element-name>) -
XML attribute (
<element-name … attribute-name="attribute value">) -
XML namespace (
xmlns='namespace-uri') -
XSD (XML Schema) constructs:
-
XML simple type: primitive values (
xs:…) -
XML complex type: structural definition (an element can require a complex type, complex types can be constructed from other types)
-
XML complex type declaration definitions: sequence, order, etc.
-
It is imperative that the developer fully understands these concepts before embarking on developing XML mappings for models.
XML elements and XML types
XML elements and XML types (for Lutaml::Model)
General
In Lutaml::Model, XML serialization mappings are defined using the xml block.
Syntax:
class Example < Lutaml::Model::Serializable
xml do
# Type-level methods
# Mapping methods
end
end| For sharing XML mappings across multiple model classes, see Reusable XML Mapping Classes. |
Defining element name (element)
The element method is the primary way to declare the XML element name ("tag name") for an XML element.
The root method was previously used for the same purpose as element, and is now an alias to element. It is considered deprecated usage due to more accurate naming of element. |
An XML mapping that does not use the element declaration means it is an "XML type".
If element is not given, but used as a root of an XML element without a tag name defined (an ad-hoc tag name can be defined in a mapping), then the snake-cased class name will be used as the tag name. |
element 'example' sets the tag name for in XML as <example>…</example>. Syntax:
xml do
element 'element-name'
endexampleclass Example < Lutaml::Model::Serializable
xml do
element 'example'
end
end> Example.new.to_xml
> #<example></example>class Ceramic < Lutaml::Model::Serializable
attribute :type, :string
xml do
element 'ceramic'
map_element 'type', to: :type
end
end
puts Ceramic.new(type: "Porcelain").to_xml
# => <ceramic><type>Porcelain</type></ceramic>The root method is maintained as a backward-compatible alias to element that also supports the mixed: and ordered: options.
| In v0.8.0 onwards, the
Note: |
(DEPRECATED) Root method with options
| This syntax is deprecated. Use the |
Syntax (deprecated):
xml do
root 'element-name', mixed: false, ordered: false # DEPRECATED
endValues:
mixed-
(deprecated)
trueto enable mixed content (text + elements). Usemixed_contentmethod instead. ordered-
(deprecated)
trueto preserve element order. Useorderedmethod instead.
root with options (use mixed_content method instead)class Paragraph < Lutaml::Model::Serializable
attribute :bold, :string, collection: true
attribute :italic, :string
xml do
element 'p'
mixed_content # DEPRECATED - use mixed_content method instead
map_element 'bold', to: :bold
map_element 'i', to: :italic
end
end Declaring an XML type (element omitted)
For XML type-only models (models used only as embedded types without their own element), simply omit the element declaration.
Syntax:
class Address < Lutaml::Model::Serializable
xml do
# No element() or root() call - this is a type-only model
sequence do
map_element 'street', to: :street
map_element 'city', to: :city
end
end
end Type-only models can only be parsed when embedded in parent models, not standalone. Attempting to call Address.from_xml(xml) will raise TypeOnlyMappingError. |
The modern pattern is to omit the element declaration entirely for type-only models. The no_root method is deprecated.
class Address < Lutaml::Model::Serializable
xml do
# No element declaration - type-only model
map_element 'street', to: :street
end
endno_root method (deprecated)class Address < Lutaml::Model::Serializable
xml do
no_root # DEPRECATED - omit element declaration instead
map_element 'street', to: :street
end
endWhen a model has no element declaration, only map_element can be used because without a root element there cannot be attributes.
class NameAndCode < Lutaml::Model::Serializable
attribute :name, :string
attribute :code, :string
xml do
# Type-only model - no element declaration needed
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> Mixed content elements (mixed_content method)
The mixed_content method explicitly enables mixed content mode.
Mixed content means that the XML element or type contains interleaved text and child elements whose order must be preserved during round-trip serialization.
This is most typically used for encoding rich-text or semantically-tagged text.
<description><p>My name is <bold>John Doe</bold>, and I'm <i>28</i> years old</p></description>A mixed content mode:
-
Preserves text nodes interspersed with elements
-
Automatically enables ordered mode (
@ordered = true) -
Required for rich text content and round-trip fidelity
Syntax:
xml do
element 'element-name'
mixed_content # Enables mixed content + ordered
end| When a model has |
mixed_content and map_content collection: trueclass MathExpression < Lutaml::Model::Serializable
attribute :content, :string
xml do
element "math"
map_content to: :content
end
end
class Paragraph < Lutaml::Model::Serializable
attribute :text, :string, collection: true (1)
attribute :stem, MathExpression, collection: true
xml do
element 'p'
mixed_content (2)
map_content to: :text
map_element 'stem', to: :stem
end
end| 1 | collection: true is required — mixed content produces multiple text segments |
| 2 | mixed_content enables ordered round-trip via element_order tracking |
Input XML:
<p>Temperature <stem>T</stem> minus <stem>T<sub>90</sub></stem></p>Round-trip preserves interleaving:
parsed = Paragraph.from_xml(xml)
parsed.text
# => ["Temperature ", " minus "]
parsed.stem.map(&:content)
# => ["T", "T90"]
# Serialize back -- text and elements appear in their original positions
serialized = parsed.to_xml
# => "<p>Temperature <stem>T</stem> minus <stem>T<sub>90</sub></stem></p>"
# Iterate in document order to access text and elements interleaved
parsed.each_mixed_content do |node|
case node
when String
puts "Text: #{node.inspect}"
when MathExpression
puts "Math: #{node.content}"
end
end
# Output:
# Text: "Temperature "
# Math: T
# Text: " minus "
# Math: T90mixed_content explicitlyclass RichText < Lutaml::Model::Serializable
attribute :bold, :string, collection: true
attribute :italic, :string, collection: true
xml do
element 'text'
mixed_content # Explicit mixed content declaration
map_element 'b', to: :bold
map_element 'i', to: :italic
end
end
xml_input = "<text>This is <b>bold</b> and <i>italic</i> text</text>"
parsed = RichText.from_xml(xml_input)
# Preserves: "This is ", "<b>bold</b>", " and ", "<i>italic</i>", " text" When to use mixed_content
Use mixed_content when an XML element contains both text nodes and child elements in arbitrary order. Common patterns include:
-
Rich text with inline formatting (
<p>text <b>bold</b> more text</p>) -
Scientific notation with inline math (
Temperature <stem>T</stem> minus …) -
Annotation elements interspersed with prose
Without mixed_content, serialization groups all text first then all elements (or vice versa), losing the original interleaving. This is the most common cause of XML round-trip failures in document-processing applications.
Iterating mixed content with index
each_mixed_content returns an Enumerator when called without a block, so you can chain standard Ruby methods like with_index, select, map:
parsed.each_mixed_content.with_index do |node, index|
puts "#{index}: #{node.inspect}"
end
# 0: "Temperature "
# 1: #<MathExpression content="T">
# 2: " minus "
# 3: #<MathExpression content="T90"> (DEPRECATED) Mixed content declaration (root method with mixed:)
To map this to Lutaml::Model we can use the mixed option in either way:
-
when defining the model;
-
when referencing the model.
| 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.
(DEPRECATED) Syntax:
xml do
element 'xml_element_name'
mixed_content
endmixed to treat root as mixed contentclass Paragraph < Lutaml::Model::Serializable
attribute :bold, :string, collection: true # allows multiple bold tags
attribute :italic, :string
xml do
element 'p'
mixed_content
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 method)
The ordered method preserves the order of XML Elements during round-trip serialization without validation.
Use ordered when: * Element order matters for your application * You need to preserve input order exactly * You DON’T want to validate/enforce a specific order
Use sequence when you need strict order validation (see [Sequence patterns]).
|
|
Syntax:
xml do
element 'xml_element_name'
ordered # Enable ordered content
endordered method to preserve element orderclass RootOrderedContent < Lutaml::Model::Serializable
attribute :bold, :string
attribute :italic, :string
attribute :underline, :string
xml do
element "RootOrderedContent"
ordered # Enable ordered content
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 (default behavior):
class RootOrderedContent < Lutaml::Model::Serializable
attribute :bold, :string
attribute :italic, :string
attribute :underline, :string
xml do
element "RootOrderedContent"
# No ordered method - order follows attribute declaration
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 # The order now follows attribute declaration order
> # <RootOrderedContent>
# <bold>bell</bold>
# <italic>384,400 km</italic>
# <underline>Moon</underline>
# </RootOrderedContent>XML content mapping
XML content mapping
Mapping elements
General
The map_element method maps an XML element to a data model attribute.
<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
endname tag to the name attributeclass Example < Lutaml::Model::Serializable
attribute :name, :string
xml do
element "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.
class RecordDate < Lutaml::Model::Serializable
attribute :content, :string
xml do
element "recordDate"
map_content to: :content
end
end
class OriginInfo < Lutaml::Model::Serializable
attribute :date_issued, RecordDate, collection: true
xml do
element "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 several element names to one attribute
Several map_element rules may target the same collection attribute to accept multiple spellings of one element (e.g. a legacy and a modern name). Parsing merges the content of every spelling in document order; serialization emits the value once, under the first declared spelling.
class Editorial < Lutaml::Model::Serializable
attribute :groups, Group, collection: true
xml do
element "metadata"
map_element "editorial-group", to: :groups
map_element "editorialgroup", to: :groups # legacy spelling
end
end
Editorial.from_xml(
"<metadata><editorialgroup>…</editorialgroup><editorial-group>…</editorial-group></metadata>"
).groups
# => [group(legacy), group(modern)] — document order The merge applies to plain element rules on collection attributes. Rules with custom deserialization methods, raw, content, or cdata are not merged — each still assigns separately, last rule winning as before. |
Using elements in different namespaces
General
When elements need different namespaces, create separate model classes for each namespace. The namespace: parameter on map_element and map_attribute has been removed. See the migration guide for details.
Elements with different namespaces
Create child model classes for elements in different namespaces:
xml do
# Parent model namespace
namespace ParentNamespace
map_element 'child', to: :child # Child type determines namespace
endclass CeramicNamespace < Lutaml::Model::XmlNamespace
uri 'https://example.com/ceramic'
prefix_default 'cer'
end
class GlazeNamespace < Lutaml::Model::XmlNamespace
uri 'https://example.com/glaze'
prefix_default 'glz'
end
# Child model with its own namespace
class Glaze < Lutaml::Model::Serializable
attribute :value, :string
xml do
element 'glaze'
namespace GlazeNamespace # Model-level namespace
map_content to: :value
end
end
class Ceramic < Lutaml::Model::Serializable
attribute :type, :string
attribute :glaze, Glaze # Typed attribute
xml do
element 'ceramic'
namespace CeramicNamespace
# This element uses parent namespace (follows element_form_default)
map_element 'type', to: :type
# This element uses Glaze's namespace (GlazeNamespace)
map_element 'glaze', to: :glaze
end
end
puts Ceramic.new(type: "Porcelain", glaze: Glaze.new(value: "Celadon")).to_xml
# => <cer:ceramic xmlns:cer="https://example.com/ceramic"
# xmlns:glz="https://example.com/glaze">
# <cer:type>Porcelain</cer:type>
# <glz:glaze>Celadon</glz:glaze>
# </cer:ceramic>Inheriting parent namespace
Use element_form_default :qualified on the namespace class to make child elements inherit the parent namespace.
class MyNamespace < Lutaml::Model::XmlNamespace
uri 'https://example.com/my'
prefix_default 'my'
element_form_default :qualified # Children inherit namespace
endelement_form_default :qualified for namespace inheritanceclass CeramicNamespace < Lutaml::Model::XmlNamespace
uri 'https://example.com/ceramic'
prefix_default 'cer'
element_form_default :qualified # All child elements inherit namespace
end
class Ceramic < Lutaml::Model::Serializable
attribute :type, :string
attribute :special_type, :string
xml do
element 'ceramic'
namespace CeramicNamespace
# Both elements inherit parent namespace (qualified)
map_element 'type', to: :type
map_element 'specialType', to: :special_type
end
end
puts Ceramic.new(type: "Porcelain", special_type: "Fine").to_xml
# => <cer:ceramic xmlns:cer="https://example.com/ceramic">
# <cer:type>Porcelain</cer:type>
# <cer:specialType>Fine</cer:specialType>
# </cer:ceramic>Mapping attributes
General
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
endmap_attribute to map the value attributeThe following class will parse the XML snippet below:
class Example < Lutaml::Model::Serializable
attribute :value, :integer
xml do
element "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 TechXmiXmlNamespace < Lutaml::Model::Xml::Namespace
uri "http://www.tech.co/XMI"
default_prefix "xl"
end
class TechXmiIntegerType < Lutaml::Model::Value::String
xml_namespace TechXmiXmlNamespace
end
class Attribute < Lutaml::Model::Serializable
attribute :value, TechXmiIntegerType
xml do
element "example"
map_attribute 'value', to: :value
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\"/>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 (with reuseable XmlNamespace):
xml do
map_element 'xml_element_name', to: :name_of_attribute,
namespace: ExampleXmlNamespaceClass
endWhere:
namespace-
The XML namespace used by this element, as an XmlNamespace class
Syntax (ad-hoc definition of namespace, results in an anonymous XmlNamespace class):
xml do
map_element 'xml_element_name', to: :name_of_attribute,
namespace: 'http://example.com/namespace',
prefix: 'prefix'
endWhere:
namespace-
The XML namespace used by this element, as a URI string
prefix-
The XML namespace prefix used by this element (optional)
namespace option to set the namespace for an elementIn this example, glz will be used for Glaze if it is added inside the Ceramic class, and glaze will be used otherwise.
class GlazeXmlNamespace < Lutaml::Model::XmlNamespace
uri 'http://example.com/glaze'
default_prefix 'glz'
end
class CeramicXmlNamespace < Lutaml::Model::XmlNamespace
uri 'http://example.com/ceramic'
default_prefix 'cera'
end
class OldGlazeXmlNamespace < Lutaml::Model::XmlNamespace
uri 'http://example.com/old_glaze'
default_prefix 'glaze'
end
class Ceramic < Lutaml::Model::Serializable
attribute :type, :string
attribute :glaze, Glaze
xml do
element 'Ceramic'
namespace CeramicXmlNamespace
map_element 'Type', to: :type
# This will use the GlazeXmlNamespace through the Glaze class
map_element 'Glaze', to: :glaze
end
end
class Glaze < Lutaml::Model::Serializable
attribute :color, :string
attribute :temperature, :integer
xml do
element 'Glaze'
namespace OldGlazeXmlNamespace
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>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
endmap_content to map content of the description tagThe following class will parse the XML snippet below:
class Example < Lutaml::Model::Serializable
attribute :description, :string
xml do
element "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>Capturing raw XML content
Lutaml::Model provides several mechanisms for capturing XML content as raw strings. Use these when you need to embed foreign XML vocabularies (SVG, MathML, XSL-FO) or preserve markup that your model doesn’t need to parse.
raw: :element on map_element — full element capture (recommended)
The raw: :element option captures a specific mapped element as a complete XML string, including its opening tag, attributes, children, and closing tag. This provides lossless round-trip fidelity — the captured string is self-contained and serialized verbatim.
xml do
map_element "svg", to: :svg_data, raw: :element
endclass SvgContainer < Lutaml::Model::Serializable
attribute :name, :string
attribute :svg_data, :string
xml do
element "container"
map_element "name", to: :name
map_element "svg", to: :svg_data, raw: :element
end
end<container>
<name>diagram</name>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect x="0" y="0" width="100" height="100" fill="red"/>
</svg>
</container>> doc = SvgContainer.from_xml(xml)
> doc.name
# => "diagram"
> doc.svg_data
# => "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\">\n <rect x=\"0\" y=\"0\" width=\"100\" height=\"100\" fill=\"red\"/>\n</svg>"The captured string round-trips correctly — serializing the model back to XML outputs the raw element verbatim without escaping or wrapping.
raw: :element also works with collection attributes:
class MultiFragment < Lutaml::Model::Serializable
attribute :fragments, :string, collection: true
xml do
element "container"
map_element "fragment", to: :fragments, raw: :element
end
endraw: :element also works on mixed_content and ordered parents. The captured string is emitted verbatim regardless of which applier dispatches the rule. This matters for round-trip serialization: when a model is parsed from XML, element_order is populated and the OrderedApplier path takes over — without the raw: :element short-circuit, each captured fragment (which already contains its own wrapper tag) would be wrapped again, producing <fragment><fragment>…</fragment></fragment>.
class WordParagraph < Lutaml::Model::Serializable
attribute :runs, :string, collection: true
xml do
element "p"
mixed_content
map_element "oMath", to: :runs, raw: :element
end
end raw: :content on map_element — inner content capture
The raw: :content option captures only the inner XML of the matched element, without the wrapper tags. The wrapper element is reconstructed from the mapping rule during serialization.
xml do
map_element "street", to: :street, raw: :content
endclass Address < Lutaml::Model::Serializable
attribute :street, :string
xml do
element "address"
map_element "street", to: :street, raw: :content
end
end<address><street><b>123</b> Main St</street></address>> doc = Address.from_xml(xml)
> doc.street
# => "<b>123</b> Main St" raw: :content is lossy — namespace prefixes declared on the wrapper element are not captured. For lossless capture, use raw: :element instead. |
Namespace behavior
Both raw: :element and raw: :content match elements by local name regardless of namespace or prefix:
-
Elements with no namespace (
<svg>) -
Elements with an
xmlnsdeclaration on themselves (<svg xmlns="…">) -
Elements inheriting a default namespace from a parent
-
Elements with an explicit namespace prefix (
<ns:svg>)
This is intentional — raw capture is designed to handle foreign XML vocabularies without needing model classes for them.
Round-trip fidelity comparison
| Aspect | raw: :element | raw: :content | |--------|----------------|-----------------| | Element name | Preserved (in string) | Reconstructed from rule | | Element attributes | Preserved (in string) | Lost unless separately mapped | | Inner content | Preserved (in string) | Preserved (in string) | | Namespace prefixes | Safe (declared in string) | May break if declared on wrapper | | Fidelity | Lossless | Lossy |
Wrapper model pattern for attribute extraction
Raw capture stores the element as a string. If you need to inspect or modify attributes on the captured element, use a wrapper model with map_all:
class SvgInner < Lutaml::Model::Serializable
attribute :raw, :string
xml do
element "svg"
map_all to: :raw
end
end
class Container < Lutaml::Model::Serializable
attribute :svg_data, SvgInner
xml do
element "container"
map_element "svg", to: :svg_data
end
end map_all — root inner XML capture
The map_all directive captures the entire inner XML of the root element into a single attribute. It is exclusive — it cannot be combined with other map_element or map_content mappings (only map_attribute is allowed).
xml do
map_all to: :name_of_attribute
endmap_allclass 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>." Deprecated: attribute :x, :string, raw: true
|
|
This legacy syntax captures the inner XML of the matched element. It is equivalent to raw: :content on the mapping. Existing code continues to work but emits a deprecation warning.
Mapping 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.
| 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:
-
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.
-
-
When an XML mapping sets
cdata: trueonmap_elementormap_content:-
On reading: The node (CDATA or text) is read as its value.
-
On writing: The value is written as a CDATA node.
-
-
When an XML mapping sets
cdata: falseonmap_elementormap_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)
endcdata to map CDATA contentThe 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
element "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> Mapping processing instructions (map_processing_instruction)
Processing Instructions (PIs) are XML nodes of the form <?target content?> that appear before the root element. They are used by XML applications to pass application-specific information.
Common use cases include:
-
xml2rfc PIs:
<?rfc strict="yes"?>,<?rfc compact="yes"?> -
xml-stylesheet PIs:
<?xml-stylesheet href="style.xsl"?> -
DocBook PIs:
<?db toc="yes"?>
Hash mode (default)
When the mapped attribute is a :hash, each PI matching the target generates one key-value pair. Multiple PIs with the same target are merged into a single hash.
This mode is extensible — adding a new PI key does not require model changes.
xml do
element "rfc"
map_element "title", to: :title
map_processing_instruction "rfc", to: :pi_settings
endWith pi_settings = { "strict" ⇒ "yes", "compact" ⇒ "yes" }, this generates:
<?rfc strict="yes"?>
<?rfc compact="yes"?>
<rfc><title>...</title></rfc>During deserialization, PIs with matching target are parsed into the hash attribute:
doc = RfcDoc.from_xml('<?rfc strict="yes"?><?rfc compact="no"?><rfc><title>Test</title></rfc>')
doc.pi_settings # => {"strict"=>"yes", "compact"=>"no"}Multiple PI targets
Multiple map_processing_instruction calls can map different PI targets to different attributes:
xml do
element "doc"
map_processing_instruction "rfc", to: :rfc_pis
map_processing_instruction "xml-stylesheet", to: :stylesheet_pis
endArray mode
When the mapped attribute is a :string collection, each PI’s raw content string is stored as-is:
attribute :rfc_pis, :string, collection: true
xml do
element "rfc"
map_processing_instruction "rfc", to: :rfc_pis
endWith input <?rfc strict="yes"?><?rfc compact="no"?>:
doc.rfc_pis # => ['strict="yes"', 'compact="no"']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
element "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>XML types (for Lutaml::Model::Value)
Value namespaces
Type-level namespaces are particularly useful for:
-
Reusable types that belong to specific namespaces (e.g., Dublin Core properties, custom XSD types)
-
Multi-namespace document structures (e.g., Office Open XML, Dublin Core metadata)
-
XSD schema generation with proper namespace imports
-
W3C-compliant round-trip serialization and deserialization
Value xml block (unified API)
Custom value types declare their XML configuration using the unified xml do … end block.
Syntax:
class CustomType < Lutaml::Model::Type::Value
xml do (1)
namespace CustomNamespace (2)
xsd_type 'CustomType' (3)
end
def self.cast(value)
# Type conversion logic
end
end| 1 | The xml block provides unified XML configuration (same API as Model classes) |
| 2 | The namespace directive associates an XmlNamespace class |
| 3 | The xsd_type directive sets the XSD type name for schema generation |
Where,
namespace-
Directive inside
xmlblock that accepts anXmlNamespaceclass. This namespace will be applied to any element or attribute using this type, unless overridden by explicit mapping namespace. Works for both serialization and deserialization. xsd_type-
Directive inside
xmlblock that sets the XSD type name. If not specified, defaults todefault_xsd_typefrom the parent class (e.g.,xs:stringforType::String).
The old class-level xml_namespace and xsd_type directives are deprecated. Use the unified xml do … end block for new code. |
Type-level namespaces are resolved during both serialization and deserialization:
During serialization (to_xml):
-
When an element or attribute uses a custom type with namespace
-
The type’s namespace is consulted if no explicit mapping namespace exists
-
Namespace declarations are added to the XML document root
-
Elements/attributes are prefixed according to namespace resolution priority
During deserialization (from_xml):
-
Namespace-qualified elements/attributes are matched against type namespaces
-
Both prefixed (
dc:title) and default namespace elements are handled -
Type namespaces work with
namespace: :inheritand explicit mappings
class EmailNamespace < Lutaml::Model::XmlNamespace
uri 'https://example.com/types/email'
prefix_default 'email'
end
class EmailType < Lutaml::Model::Type::String
xml do
namespace EmailNamespace
xsd_type 'EmailAddress'
end
def self.cast(value)
email = super(value)
unless email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
raise Lutaml::Model::TypeError, "Invalid email: #{email}"
end
email.downcase
end
end
class Contact < Lutaml::Model::Serializable
attribute :email, EmailType
xml do
element "contact"
map_element 'email', to: :email # Uses EmailNamespace automatically
end
end
# Serialization output:
contact = Contact.new(email: "user@example.com")
puts contact.to_xml
# => <contact xmlns:email="https://example.com/types/email">
# <email:email>user@example.com</email:email>
# </contact>
# Deserialization (round-trip):
parsed = Contact.from_xml(contact.to_xml)
parsed.email # => "user@example.com"
parsed === contact # => trueInstance serialization
General
XML serialization is controlled via the to_xml method on Lutaml::Model::Serializable and Lutaml::Model::Value objects. instances.
Syntax:
instance.to_xml(options)Where,
options-
Hash of serialization options (see below for details)
Namespace prefix behavior
General
The prefix: option in to_xml controls whether the root element’s namespace is rendered as a default namespace (no prefix) or with a prefix.
This is a serialization-time decision that allows the same model to output clean W3C-compliant XML (default namespace) or prefixed XML for legacy system compatibility.
| Only the root element’s own namespace can be set as default. Other namespaces in scope MUST use their defined prefixes. |
Default behavior: clean XML with default namespace
By default, to_xml renders the root element’s namespace as a default namespace (xmlns="…") with no prefix. This produces clean, W3C-compliant XML.
| A namespace is only rendered if and only if the element itself is assigned an XML namespace. |
Syntax:
instance.to_xml (1)| 1 | No prefix: option uses default namespace (no prefix) |
class AppNamespace < Lutaml::Model::XmlNamespace
uri 'http://schemas.openxmlformats.org/officeDocument/2006/extended-properties'
prefix_default 'app'
end
class Properties < Lutaml::Model::Serializable
attribute :template, :string
xml do
element "Properties"
namespace AppNamespace
map_element "Template", to: :template
end
end
props = Properties.new(template: "Normal.dotm")
puts props.to_xmlOutput:
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">
<Template>Normal.dotm</Template>
</Properties> Clean XML with no prefixes. The namespace is declared as default (xmlns="…"). |
Using defined default prefix
To use the prefix defined in XmlNamespace.prefix_default of the element, pass prefix: true:
Syntax:
instance.to_xml(prefix: true) (1)| 1 | Uses prefix_default from XmlNamespace class |
props = Properties.new(template: "Normal.dotm")
puts props.to_xml(prefix: true)Output:
<app:Properties xmlns:app="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">
<app:Template>Normal.dotm</app:Template>
</app:Properties>| All elements in the same namespace use the "app" prefix. |
Using custom prefix
To use a specific custom prefix (overriding prefix_default), pass a string:
Syntax:
instance.to_xml(prefix: "custom") (1)| 1 | Uses provided custom prefix string |
props = Properties.new(template: "Normal.dotm")
puts props.to_xml(prefix: "extended")Output:
<extended:Properties xmlns:extended="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">
<extended:Template>Normal.dotm</extended:Template>
</extended:Properties> Custom prefix of "extended" used instead of the original "app". |
to_xml method prefix: option values
The prefix: option accepts the following values:
| Value | Behavior |
|---|---|
(not specified) or | Use default namespace (xmlns="…") with no prefix. This is the default behavior. |
| Use |
| Use the provided custom prefix string |
Element and attribute qualification
General
XML namespace qualification determines whether elements and attributes in instance documents must include namespace prefixes. Following W3C XML Schema specifications, qualification is controlled at three levels:
-
Namespace-level defaults via
element_form_defaultandattribute_form_default -
Element/attribute-level overrides via
form:option -
Global elements/attributes (always qualified when in a namespace)
Qualification rules
W3C qualification semantics
Per W3C XML Schema specification:
-
Global elements (declared at schema root): Always qualified when in a namespace
-
Local elements (declared within a type): Follow
elementFormDefault -
Global attributes (declared at schema root): Always qualified when in a namespace
-
Local attributes (declared within a type): Follow
attributeFormDefault
Default behavior
The default behavior follows W3C conventions:
-
element_form_default::unqualified(local elements not prefixed) -
attribute_form_default::unqualified(local attributes not prefixed)
This means child elements and attributes are not namespace-qualified by default, even when the parent element is.
class CeramicNamespace < Lutaml::Model::XmlNamespace
uri 'https://example.com/ceramic'
prefix_default 'cer'
# element_form_default defaults to :unqualified
# attribute_form_default defaults to :unqualified
end
class Ceramic < Lutaml::Model::Serializable
attribute :type, :string
attribute :glaze, :string
xml do
element 'ceramic'
namespace CeramicNamespace
map_element 'type', to: :type
map_attribute 'glaze', to: :glaze
end
end
puts Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml
# => <cer:ceramic xmlns:cer="https://example.com/ceramic" glaze="Clear">
# <type>Porcelain</type>
# </cer:ceramic>
# NOTE: <cer:ceramic> is qualified (global element)
# <type> is unqualified (local element, elementFormDefault=unqualified)
# glaze="" is unqualified (local attribute, attributeFormDefault=unqualified)Namespace-level qualification control
Set qualification defaults for all local elements and attributes in the namespace.
class CeramicNamespace < Lutaml::Model::XmlNamespace
uri 'https://example.com/ceramic'
prefix_default 'cer'
element_form_default :qualified # All local elements must be qualified
end
class Ceramic < Lutaml::Model::Serializable
attribute :type, :string
attribute :color, :string
xml do
element 'ceramic'
namespace CeramicNamespace
map_element 'type', to: :type
map_element 'color', to: :color
end
end
puts Ceramic.new(type: "Porcelain", color: "White").to_xml
# => <cer:ceramic xmlns:cer="https://example.com/ceramic">
# <cer:type>Porcelain</cer:type>
# <cer:color>White</cer:color>
# </cer:ceramic>
# Now <cer:type> and <cer:color> are qualifiedElement/attribute-level qualification override
Override namespace defaults using the form: option on individual mappings.
Syntax:
xml do
map_element 'name', to: :name, form: :qualified # or :unqualified
map_attribute 'id', to: :id, form: :qualified # or :unqualified
endclass CeramicNamespace < Lutaml::Model::XmlNamespace
uri 'https://example.com/ceramic'
prefix_default 'cer'
element_form_default :unqualified # Default: no prefix on local elements
end
class Ceramic < Lutaml::Model::Serializable
attribute :type, :string
attribute :glaze, :string
attribute :id, :string
xml do
element 'ceramic'
namespace CeramicNamespace
# Override specific element to be qualified
map_element 'type', to: :type, form: :qualified
# This element follows namespace default (unqualified)
map_element 'glaze', to: :glaze
# Force attribute to be qualified (unusual but supported)
map_attribute 'id', to: :id, form: :qualified
end
end
puts Ceramic.new(type: "Porcelain", glaze: "Clear", id: "C001").to_xml
# => <cer:ceramic xmlns:cer="https://example.com/ceramic" cer:id="C001">
# <cer:type>Porcelain</cer:type>
# <glaze>Clear</glaze>
# </cer:ceramic>
# NOTE: <cer:type> qualified via form: :qualified override
# <glaze> unqualified via namespace default
# cer:id qualified via form: :qualified overrideUse cases for qualification
Qualified elements (:qualified):
-
Ensures elements are unambiguous across namespace boundaries
-
Required when mixing elements from multiple namespaces
-
Common in complex, multi-namespace schemas
Unqualified elements (:unqualified):
-
Simpler, more readable XML for single-namespace documents
-
Reduces verbosity when namespace context is clear
-
W3C default for local elements
Qualified attributes (unusual):
-
Rarely needed in practice
-
Only when attributes are from different namespace than parent element
-
Most schemas use
:unqualifiedfor attributes