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 namespace

General

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

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'
end
Example 1. Setting the element name to example
class Example < Lutaml::Model::Serializable
  xml do
    element 'example'
  end
end
> Example.new.to_xml
> #<example></example>
Example 2. Setting ad-hoc element names in a mapping
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, these options are deprecated in favor of:

  • the mixed_content method replaces mixed: true;

  • the sequence do block replaces the ordered: true option

Syntax:

xml do
  root 'element-name', mixed: false, ordered: false
end

Values:

mixed

(optional) true to enable mixed content (text + elements), false otherwise (default)

ordered

(optional) true to preserve element order, false otherwise (default)

Example 3. Using root with options
class Paragraph < Lutaml::Model::Serializable
  attribute :bold, :string, collection: true
  attribute :italic, :string

  xml do
    root 'p', mixed: true  # Enable mixed content
    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 NoRootMappingError.

The no_root method is deprecated.

Syntax:

xml do
  no_root
end
Syntax for no_root method (deprecated)
class Address < Lutaml::Model::Serializable
  xml do
    no_root  # DEPRECATED
    map_element 'street', to: :street
  end
end

When no_root is used, 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
    no_root
    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 is whitespace and order sensitive, and therefore preserves them.

This is most typically used 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

  • Required for rich text content

Syntax:

xml do
  element 'element-name'
  mixed_content  # Enables mixed content + ordered
end
Example 4. Using mixed_content explicitly
class 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"

(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
  root 'xml_element_name', mixed: true
end
Example 5. Applying mixed to treat root as mixed content
class Paragraph < Lutaml::Model::Serializable
  attribute :bold, :string, collection: true # allows multiple bold tags
  attribute :italic, :string

  xml do
    root 'p', mixed: true

    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: true maintains the order of XML Elements, while mixed: true preserves the order of XML Elements and Content.

When both options are used, mixed: true takes precedence.

To specify ordered content, the ordered: true option needs to be set at the xml block’s root method.

Syntax:

xml do
  root 'xml_element_name', ordered: true
end
Example 6. Applying ordered to treat root as ordered content
class RootOrderedContent < Lutaml::Model::Serializable
  attribute :bold, :string
  attribute :italic, :string
  attribute :underline, :string

  xml do
    root "RootOrderedContent", ordered: true
    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 True:

class RootOrderedContent < Lutaml::Model::Serializable
  attribute :bold, :string
  attribute :italic, :string
  attribute :underline, :string

  xml do
    root "RootOrderedContent"
    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.

To handle the <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
end
Example 7. Mapping the name tag to the name attribute
class Example < Lutaml::Model::Serializable
  attribute :name, :string

  xml do
    root '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.

Example 8. The mapped tag name is used as the root name
class RecordDate < Lutaml::Model::Serializable
  attribute :content, :string

  xml do
    root "recordDate"
    map_content to: :content
  end
end

class OriginInfo < Lutaml::Model::Serializable
  attribute :date_issued, RecordDate, collection: true

  xml do
    root "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>
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
end
Example 9. Using different namespaces for elements
class 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.

Syntax:
class MyNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/my'
  prefix_default 'my'
  element_form_default :qualified  # Children inherit namespace
end
Example 10. Using element_form_default :qualified for namespace inheritance
class 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
end
Example 11. Using map_attribute to map the value attribute

The following class will parse the XML snippet below:

class Example < Lutaml::Model::Serializable
  attribute :value, :integer

  xml do
    root '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
    root '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
end

Where:

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'
end

Where:

namespace

The XML namespace used by this element, as a URI string

prefix

The XML namespace prefix used by this element (optional)

Example 12. Using the namespace option to set the namespace for an element

In 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
end
Example 13. Using map_content to map content of the description tag

The following class will parse the XML snippet below:

class Example < Lutaml::Model::Serializable
  attribute :description, :string

  xml do
    root '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>

Mapping entire XML element into an attribute

The map_all tag in XML mapping captures and maps all content within an XML element 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 of the XML element at all, and instead handle it as an XML string.

The corresponding method for key-value formats is at [key-value-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 in the case where the content of an XML element is not to be handled by a Lutaml::Model::Serializable object.

This feature is commonly used with custom methods or a custom model object to handle the content.

This includes:

  • nested tags

  • attributes

  • text nodes

The map_all tag is exclusive and cannot be combined with other mappings (map_element, map_content) except for map_attribute for the same element, ensuring it captures the entire inner XML content.

An error is raised if map_all is defined alongside any other mapping in the same XML mapping context.

Syntax:

xml do
  map_all to: :name_of_attribute
end
Example 14. Mapping all the content using map_all
class 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>."

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:

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

  2. When an XML mapping sets cdata: true on map_element or map_content:

    • On reading: The node (CDATA or text) is read as its value.

    • On writing: The value is written as a CDATA node.

  3. When an XML mapping sets cdata: false on map_element or map_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)
end
Example 15. Using cdata to map CDATA content

The 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
    root '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>

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
    root '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)

General

LutaML provides a number of methods to customize the XML role of custom Values.

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 xml block that accepts an XmlNamespace class. 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 xml block that sets the XSD type name. If not specified, defaults to default_xsd_type from the parent class (e.g., xs:string for Type::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: :inherit and explicit mappings

Example 16. Using namespace directive with a custom type (unified API)
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
    root '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  # => true

Instance 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)
Example 17. Default namespace output (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
    root "Properties"
    namespace AppNamespace
    map_element "Template", to: :template
  end
end

props = Properties.new(template: "Normal.dotm")
puts props.to_xml

Output:

<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
Example 18. Output with defined default prefix
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
Example 19. Output with custom prefix
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 nil or false

Use default namespace (xmlns="…​") with no prefix. This is the default behavior.

true

Use prefix_default from XmlNamespace class

"string"

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:

  1. Namespace-level defaults via element_form_default and attribute_form_default

  2. Element/attribute-level overrides via form: option

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

Example 20. Default qualification behavior
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.

Example 21. Qualifying all local elements
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 qualified

Element/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
end
Example 22. Overriding qualification per element/attribute
class 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 override

Use 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 :unqualified for attributes