Purpose

This document demonstrates how to handle complex namespace scenarios commonly found in Office Open XML (OOXML) documents using Type-level namespace definitions. These patterns enable round-trip parsing of XML documents where different attributes use different namespaces from their parent elements.

What is OOXML?

Office Open XML (OOXML) is the file format used by Microsoft Office applications (Word, Excel, PowerPoint). OOXML documents use multiple XML namespaces extensively, often with:

  • Root elements in one namespace

  • Child elements in different namespaces

  • Attributes that belong to yet other namespaces

  • Complex nested structures with mixed namespaces

Type-Level Namespace Definitions

Type-level namespaces allow you to define namespace information directly on custom Type classes. This is essential for handling OOXML and similar formats where:

  1. Different attributes need different namespace prefixes

  2. Namespaces are determined by the data type, not the containing element

  3. Round-trip parsing must preserve all namespace declarations

Defining an XmlNamespace Class

class ContactNamespace < Lutaml::Model::XmlNamespace
  uri "https://example.com/schemas/contact/v1"
  schema_location "https://example.com/schemas/contact/v1/contact.xsd"
  prefix_default "ct"
end

Where:

uri

The XML namespace URI

schema_location

(Optional) Location of the XML Schema Definition (XSD)

prefix_default

The default prefix to use when serializing

Attaching Namespace to a Type

class GivenNameType < Lutaml::Model::Type::String
  xml_namespace(ContactNamespace)
end

Example 1: Contact with 2 Namespaces

This example demonstrates a contact data model using two separate namespaces:

Complete Working Example

require "lutaml/model"

# Define namespace classes
class ContactNamespace < Lutaml::Model::XmlNamespace
  uri "https://example.com/schemas/contact/v1"
  schema_location "https://example.com/schemas/contact/v1/contact.xsd"
  prefix_default "ct"
end

class NameAttributeNamespace < Lutaml::Model::XmlNamespace
  uri "https://example.com/schemas/name-attributes/v1"
  schema_location "https://example.com/schemas/name-attributes/v1/name-attributes.xsd"
  prefix_default "name"
end

# Define Type classes with namespaces
class GivenNameType < Lutaml::Model::Type::String
  xml_namespace(ContactNamespace)
end

class SurnameType < Lutaml::Model::Type::String
  xml_namespace(ContactNamespace)
end

class NamePrefixType < Lutaml::Model::Type::String
  xml_namespace(NameAttributeNamespace)
end

# Define PersonName Model
class PersonName < Lutaml::Model::Serializable
  attribute :given_name, GivenNameType
  attribute :surname, SurnameType
  attribute :prefix, NamePrefixType
  attribute :suffix, :string

  xml do
    element "personName"
    map_element "givenName", to: :given_name
    map_element "surname", to: :surname
    map_attribute "prefix", to: :prefix
    map_attribute "suffix", to: :suffix
  end
end

# Define Contact Model
class Contact < Lutaml::Model::Serializable
  attribute :person_name, PersonName

  xml do
    element "ContactInfo"
    map_element "personName", to: :person_name
  end
end

Input XML with Default Prefixes

<ContactInfo>
  <personName xmlns:ct="https://example.com/schemas/contact/v1"
              xmlns:name="https://example.com/schemas/name-attributes/v1"
              name:prefix="Dr." suffix="Jr.">
    <ct:givenName>John</ct:givenName>
    <ct:surname>Doe</ct:surname>
  </personName>
</ContactInfo>

Parsing and Round-Trip

# Parse the XML
instance = Contact.from_xml(xml_string)

# Access the data
puts instance.person_name.given_name  # => "John"
puts instance.person_name.surname     # => "Doe"
puts instance.person_name.prefix      # => "Dr."
puts instance.person_name.suffix      # => "Jr."

# Serialize back to XML (preserves namespaces)
serialized = instance.to_xml
# The output will match the input structure with all namespaces preserved

Input XML with Custom Prefixes

The system also handles custom prefixes correctly:

<ContactInfo>
  <personName xmlns:CT="https://example.com/schemas/contact/v1"
              xmlns:NA="https://example.com/schemas/name-attributes/v1"
              NA:prefix="Dr." suffix="Jr.">
    <CT:givenName>John</CT:givenName>
    <CT:surname>Doe</CT:surname>
  </personName>
</ContactInfo>

When parsed and re-serialized, the system will use the default prefixes (ct and name) defined in the namespace classes, ensuring consistency.

Example 2: OOXML Core Properties with 4 Namespaces

This example demonstrates an OOXML Core Properties document using four namespaces:

Complete Working Example

require "lutaml/model"

# Define namespace classes
class CpNamespace < Lutaml::Model::XmlNamespace
  uri "http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
  prefix_default "cp"
end

class DcNamespace < Lutaml::Model::XmlNamespace
  uri "http://purl.org/dc/elements/1.1/"
  prefix_default "dc"
end

class DctermsNamespace < Lutaml::Model::XmlNamespace
  uri "http://purl.org/dc/terms/"
  prefix_default "dcterms"
end

class XsiNamespace < Lutaml::Model::XmlNamespace
  uri "http://www.w3.org/2001/XMLSchema-instance"
  prefix_default "xsi"
end

# Define Type classes with namespaces
class DcTitleType < Lutaml::Model::Type::String
  xml_namespace(DcNamespace)
end

class DcCreatorType < Lutaml::Model::Type::String
  xml_namespace(DcNamespace)
end

class CpLastModifiedByType < Lutaml::Model::Type::String
  xml_namespace(CpNamespace)
end

class CpRevisionType < Lutaml::Model::Type::Integer
  xml_namespace(CpNamespace)
end

class XsiTypeType < Lutaml::Model::Type::String
  xml_namespace(XsiNamespace)
end

# Define DctermsCreated Model
class DctermsCreated < Lutaml::Model::Serializable
  attribute :value, :date_time
  attribute :type, XsiTypeType

  xml do
    element "created"
    namespace DctermsNamespace
    map_attribute "type", to: :type
    map_content to: :value
  end
end

# Define DctermsModified Model
class DctermsModified < Lutaml::Model::Serializable
  attribute :value, :date_time
  attribute :type, XsiTypeType

  xml do
    element "modified"
    namespace DctermsNamespace
    map_attribute "type", to: :type
    map_content to: :value
  end
end

# Define CoreProperties root model
class CoreProperties < Lutaml::Model::Serializable
  attribute :title, DcTitleType
  attribute :creator, DcCreatorType
  attribute :last_modified_by, CpLastModifiedByType
  attribute :revision, CpRevisionType
  attribute :created, DctermsCreated
  attribute :modified, DctermsModified

  xml do
    element "coreProperties"
    map_element "title", to: :title
    map_element "creator", to: :creator
    map_element "lastModifiedBy", to: :last_modified_by
    map_element "revision", to: :revision
    map_element "created", to: :created
    map_element "modified", to: :modified
  end
end

Input XML (OOXML Core Properties)

<coreProperties xmlns:dc="http://purl.org/dc/elements/1.1/"
                xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties">
  <dc:title>Untitled</dc:title>
  <dc:creator>Uniword</dc:creator>
  <cp:lastModifiedBy>Uniword</cp:lastModifiedBy>
  <cp:revision>1</cp:revision>
  <dcterms:created xmlns:dcterms="http://purl.org/dc/terms/"
                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                   xsi:type="dcterms:W3CDTF">2025-11-13T17:11:03+00:00</dcterms:created>
  <dcterms:modified xmlns:dcterms="http://purl.org/dc/terms/"
                    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                    xsi:type="dcterms:W3CDTF">2025-11-13T17:11:03+00:00</dcterms:modified>
</coreProperties>

Parsing and Round-Trip

# Parse the OOXML Core Properties
instance = CoreProperties.from_xml(xml_string)

# Access the data
puts instance.title              # => "Untitled"
puts instance.creator            # => "Uniword"
puts instance.last_modified_by   # => "Uniword"
puts instance.revision           # => 1
puts instance.created.value      # => DateTime instance
puts instance.created.type       # => "dcterms:W3CDTF"
puts instance.modified.value     # => DateTime instance
puts instance.modified.type      # => "dcterms:W3CDTF"

# Serialize back to XML (preserves all 4 namespaces)
serialized = instance.to_xml

# Parse again to verify round-trip
reparsed = CoreProperties.from_xml(serialized)
# All values and namespaces will be preserved

Creating New Instances

# Create nested elements
created = DctermsCreated.new(
  value: DateTime.parse("2025-11-13T17:11:03Z"),
  type: "dcterms:W3CDTF"
)

modified = DctermsModified.new(
  value: DateTime.parse("2025-11-13T17:11:03Z"),
  type: "dcterms:W3CDTF"
)

# Create the main document
document = CoreProperties.new(
  title: "Test Document",
  creator: "Test Author",
  last_modified_by: "Test Modifier",
  revision: 1,
  created: created,
  modified: modified
)

# Serialize to XML with all namespaces
xml = document.to_xml

Example 3: Same-Named Elements in Different Namespaces

Word documents often contain elements with the same local name but different namespaces, such as w14:docId and w15:docId. Lutaml::Model handles this correctly during round-trip serialization.

The Challenge

<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
            xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
            xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml">
  <w14:docId w14:val="4C6D0839"/>
  <w15:chartTrackingRefBased/>
  <w15:docId w15:val="{A1B2C3D4}"/>
</w:settings>

Both <w14:docId> and <w15:docId> have the same local name docId. Without namespace-aware matching, serialization would lose one of them.

Complete Working Example

require "lutaml/model"

# Define namespaces
class W14Namespace < Lutaml::Xml::W3c::XmlNamespace
  uri "http://schemas.microsoft.com/office/word/2010/wordml"
  prefix_default "w14"
  element_form_default :qualified
end

class W15Namespace < Lutaml::Xml::W3c::XmlNamespace
  uri "http://schemas.microsoft.com/office/word/2012/wordml"
  prefix_default "w15"
  element_form_default :qualified
end

class WNamespace < Lutaml::Xml::W3c::XmlNamespace
  uri "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
  prefix_default "w"
  element_form_default :qualified
end

# W14 DocId model (in w14 namespace)
class W14DocId < Lutaml::Model::Serializable
  attribute :val, :string

  xml do
    element "docId"
    namespace W14Namespace
    map_attribute "val", to: :val
  end
end

# W15 DocId model (in w15 namespace)
class W15DocId < Lutaml::Model::Serializable
  attribute :val, :string

  xml do
    element "docId"
    namespace W15Namespace
    map_attribute "val", to: :val
  end
end

# Settings model with same-named elements
class Settings < Lutaml::Model::Serializable
  attribute :w14_doc_id, W14DocId
  attribute :w15_doc_id, W15DocId
  attribute :chart_tracking, :boolean

  xml do
    element "settings"
    namespace WNamespace
    namespace_scope [W14Namespace, W15Namespace]

    map_element "docId", to: :w14_doc_id
    map_element "chartTrackingRefBased", to: :chart_tracking
    map_element "docId", to: :w15_doc_id
  end
end

Round-Trip Serialization

xml = <<~XML
  <w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
              xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
              xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml">
    <w14:docId w14:val="4C6D0839"/>
    <w15:chartTrackingRefBased/>
    <w15:docId w15:val="{A1B2C3D4}"/>
  </w:settings>
XML

# Parse - both elements are correctly captured
settings = Settings.from_xml(xml)
settings.w14_doc_id.val   # => "4C6D0839"
settings.w15_doc_id.val  # => "{A1B2C3D4}"
settings.chart_tracking   # => true

# Round-trip preserves both elements
serialized = settings.to_xml
reparsed = Settings.from_xml(serialized)

reparsed.w14_doc_id.val  # => "4C6D0839" (preserved!)
reparsed.w15_doc_id.val  # => "{A1B2C3D4}" (preserved!)

How It Works

  1. Separate model classes: Each namespace variant gets its own model class with its own namespace

  2. Namespace capture: element_order captures the namespace URI for each element during parsing

  3. Namespace-aware matching: Serialization matches elements by both name AND namespace URI

  4. Backward compatibility: Name-only matching is used when namespace info is unavailable

Key Concepts

Type-Level vs Mapping-Level Namespaces

Level When to Use Example

Type-Level

Data type determines namespace (OOXML pattern)

DcTitleType always uses dc namespace

Mapping-Level

Element/attribute location determines namespace

map_element "title", to: :title, namespace: "…​"

Namespace Inheritance

When a Type has an xml_namespace defined:

  1. All attributes of that Type will use that namespace

  2. The namespace is automatically applied during serialization

  3. The namespace is preserved during round-trip parsing

Nested Element Namespaces

Notice in the OOXML example:

<dcterms:created xmlns:dcterms="http://purl.org/dc/terms/"
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xsi:type="dcterms:W3CDTF">...</dcterms:created>

The <dcterms:created> element declares its own namespaces and uses a different namespace (xsi) for its type attribute. This is handled correctly because:

  • The element namespace comes from the Model’s xml do …​ namespace …​ declaration

  • The attribute namespace comes from the Type’s xml_namespace declaration

Best Practices

1. Define Namespace Classes Once

# Good: Reusable namespace class
class DcNamespace < Lutaml::Model::XmlNamespace
  uri "http://purl.org/dc/elements/1.1/"
  prefix_default "dc"
end

# Use it for multiple Types
class DcTitleType < Lutaml::Model::Type::String
  xml_namespace(DcNamespace)
end

class DcCreatorType < Lutaml::Model::Type::String
  xml_namespace(DcNamespace)
end

2. Use Descriptive Type Names

# Good: Clear what namespace this Type belongs to
class DcTitleType < Lutaml::Model::Type::String
  xml_namespace(DcNamespace)
end

# Avoid: Generic name without context
class TitleType < Lutaml::Model::Type::String
  xml_namespace(DcNamespace)
end

3. Model Structure Reflects XML Structure

# The model hierarchy should match the XML hierarchy
class CoreProperties < Lutaml::Model::Serializable
  attribute :created, DctermsCreated  # Nested model

  xml do
    map_element "created", to: :created  # Maps to nested element
  end
end

4. Test Round-Trip Parsing

Always verify that parsing and re-serialization preserves:

  • All namespace declarations

  • All namespace prefixes

  • All data values

  • XML structure integrity

# Round-trip test pattern
original_xml = File.read("input.xml")
instance = CoreProperties.from_xml(original_xml)
serialized_xml = instance.to_xml
reparsed = CoreProperties.from_xml(serialized_xml)

# Verify data integrity
expect(reparsed.title).to eq(instance.title)
# ... verify all attributes ...

# Verify XML structure
expect(serialized_xml).to be_xml_equivalent_to(original_xml)

Common Issues and Solutions

Issue: Namespace Prefix Not Applied

Problem: Elements appear without namespace prefixes

Solution: Ensure the Type class has xml_namespace defined:

class MyType < Lutaml::Model::Type::String
  xml_namespace(MyNamespace)  # Don't forget this!
end

Same-Named Elements in Different Namespaces

When an OOXML document contains elements with the same local name but different namespace URIs (e.g. w14:docId and w15:docId), Lutaml::Model correctly distinguishes them during round-trip serialization.

This requires defining a separate Serializable model class for each namespace variant.

require "lutaml/model"

# Define OOXML namespaces
class WNamespace < Lutaml::Model::XmlNamespace
  uri "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
  prefix_default "w"
  element_form_default :qualified
end

class W14Namespace < Lutaml::Model::XmlNamespace
  uri "http://schemas.microsoft.com/office/word/2010/wordml"
  prefix_default "w14"
  element_form_default :qualified
end

class W15Namespace < Lutaml::Model::XmlNamespace
  uri "http://schemas.microsoft.com/office/word/2012/wordml"
  prefix_default "w15"
  element_form_default :qualified
end

# W14 DocId model (in w14 namespace)
class W14DocId < Lutaml::Model::Serializable
  attribute :val, :string

  xml do
    element "docId"
    namespace W14Namespace
    map_attribute "val", to: :val
  end
end

# W15 DocId model (in w15 namespace)
class W15DocId < Lutaml::Model::Serializable
  attribute :val, :string

  xml do
    element "docId"
    namespace W15Namespace
    map_attribute "val", to: :val
  end
end

# Settings model with two map_element entries sharing the same name
class Settings < Lutaml::Model::Serializable
  attribute :w14_doc_id, W14DocId
  attribute :w15_doc_id, W15DocId

  xml do
    element "settings"
    namespace WNamespace
    namespace_scope [W14Namespace, W15Namespace]

    # Each map_element maps to a distinct Serializable model type.
    # The namespace is picked up from the attribute's type, not from here.
    map_element "docId", to: :w14_doc_id
    map_element "docId", to: :w15_doc_id
  end
end
# Parse XML with both namespaced docId elements
xml = <<~XML
  <w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
              xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml"
              xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml">
    <w14:docId w14:val="4C6D0839"/>
    <w15:docId w15:val="{A1B2C3D4}"/>
  </w:settings>
XML

settings = Settings.from_xml(xml)
settings.w14_doc_id.val   # => "4C6D0839"
settings.w15_doc_id.val   # => "{A1B2C3D4}"

# Round-trip correctly serializes each element to its own namespace
serialized = settings.to_xml
reparsed = Settings.from_xml(serialized)

reparsed.w14_doc_id.val   # => "4C6D0839" (preserved!)
reparsed.w15_doc_id.val   # => "{A1B2C3D4}" (preserved!)
Each namespace variant needs its own Serializable class

The namespace-aware matching works because each map_element targets an attribute of a distinct type (W14DocId vs W15DocId). The rule compiler extracts the namespace from each type’s XML mapping, so the two map_element "docId" entries correctly serialize to w14:docId and w15:docId respectively.

If both attributes shared the same type, they would share the same namespace and one would overwrite the other.

Key Point: Serializable Model Namespaces

For Serializable model attributes, the namespace is stored on the model’s XML mapping (accessible via attr_type.mappings_for(:xml, register_id).namespace_class), not directly on the type class. This is different from Type::Value subclasses which have namespace_class directly on the class.

The rule compiler handles this automatically, but understanding the distinction is important when debugging namespace-related issues.

Issue: Multiple Namespace Declarations

Problem: Same namespace declared multiple times

Solution: The system automatically consolidates namespace declarations. If you see duplicates, ensure you’re using the same namespace instance:

# Good: Single namespace instance
CONTACT_NS = ContactNamespace

class GivenNameType < Lutaml::Model::Type::String
  xml_namespace(CONTACT_NS)
end

class SurnameType < Lutaml::Model::Type::String
  xml_namespace(CONTACT_NS)
end

Issue: Round-Trip Changes Prefixes

Problem: Input uses DC but output uses dc

Solution: This is expected behavior. The system normalizes to the prefix_default defined in the namespace class. The namespace URI is preserved, which is what matters for XML equivalence.

Testing Tools

The test suite provides XML equivalence checking:

expect(actual_xml).to be_xml_equivalent_to(expected_xml)

This matcher:

  • Ignores whitespace differences

  • Normalizes namespace prefixes

  • Verifies namespace URIs match

  • Compares element structure and content

See Also