Introduction

This guide provides comprehensive information about XML namespace management in Lutaml::Model, including the namespace_scope directive, W3C compliance, and best practices for multi-namespace documents.

This guide focuses on namespace declaration and scope. For the element_form_default / form: qualification system — including the precedence ladder and form: behavior on nested Serializable children — see the companion guide: XML Namespace Qualification and Prefix Control.

Understanding XML Namespaces

XML namespaces provide a method to avoid element name conflicts by qualifying names used in XML documents through a URI reference.

Namespace Declaration Formats

Default namespace format:

<element xmlns="http://example.com/ns">
  <child>Content</child>  <!-- Inherits default namespace -->
</element>

Prefix namespace format:

<prefix:element xmlns:prefix="http://example.com/ns">
  <prefix:child>Content</prefix:child>
</element>

The prefix: Serialization Option

When serializing a model to XML using to_xml, you can control how the root element’s namespace is rendered using the prefix: option. 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.

Prefix Option Values

Value Behavior

nil (not specified) - Round-trip mode

Preserve input format during serialization. If the original XML used a prefix (e.g., <oasis:table xmlns:oasis="urn:alias">), the output will use a prefix. If the original XML used default namespace, the output will use default namespace. The original namespace URI (canonical or alias) is also preserved.

false

Force default namespace (xmlns="…​") with no prefix on the element. Disables format preservation.

true

Force prefix format using prefix_default from the XmlNamespace class. Uses the namespace’s defined default prefix (e.g., "oasis").

:default

Explicitly force default namespace format (xmlns="…​") with no prefix. Same as false but more semantic. Use this when you explicitly want default format override.

"string"

Use a custom prefix string (e.g., "custom"). Overrides both prefix_default and any input format. Useful when a legacy system requires a specific prefix.

Round-trip Fidelity

For round-trip fidelity (parsing XML and re-serializing), the serializer preserves both the namespace format and the exact namespace URI from the original input:

  • If the input XML used an alias URI (e.g., urn:oasis:names:tc:xml:table), the output will use that alias URI

  • If the input XML used a canonical URI, the output will use the canonical URI

  • The prefix: option only controls whether to use prefix format or default format, NOT which URI to use

When using namespaces with uri_aliases, the system maintains a mapping between canonical URIs and their aliases. For example, a namespace might have:

When round-tripping, the original URI (alias or canonical) is preserved regardless of the prefix: option.

Examples

# Input: <oasis:table xmlns:oasis="urn:oasis:names:tc:xml:table">

# Round-trip: preserve input format (prefix + alias URI)
model.to_xml
# => <oasis:table xmlns:oasis="urn:oasis:names:tc:xml:table"/>

# Force prefix: use prefix_default but preserve alias URI
model.to_xml(prefix: true)
# => <oasis:table xmlns:oasis="urn:oasis:names:tc:xml:table"/>

# Force default: use default namespace but preserve alias URI
model.to_xml(prefix: :default)
# => <table xmlns="urn:oasis:names:tc:xml:table"/>

# Custom prefix: use custom prefix but preserve alias URI
model.to_xml(prefix: "custom")
# => <custom:table xmlns:custom="urn:oasis:names:tc:xml:table"/>

The namespace_scope Directive

General

The namespace_scope directive controls WHERE namespace declarations appear in serialized XML. By default, Lutaml::Model follows the minimal-subtree principle: namespaces are declared locally (as close to their usage as possible).

With namespace_scope, you can consolidate (hoist) multiple namespace declarations at a parent element for cleaner, more compact XML.

namespace_scope grants ELIGIBILITY to hoist namespaces to the root, not automatic hoisting. Namespaces are only declared if actually used in the document (:auto mode) unless explicitly forced (:always mode).

Syntax

xml do
  namespace RootNamespace

  # Simple list (all use :auto mode)
  namespace_scope [Namespace1, Namespace2, Namespace3]

  # Per-namespace control with hash format
  namespace_scope [
    { namespace: Namespace1, declare: :always },
    { namespace: Namespace2, declare: :auto },
    Namespace3  # Can mix hash and class
  ]
end

Declaration Modes

Mode Behavior

:auto (default)

Namespace declared only if actually used in elements or attributes. Most common mode for clean, minimal XML.

:always

Namespace always declared at root, even if unused. Required by some formats like Office Open XML that mandate specific namespace presence.

:never

Namespace never declared. Error raised if namespace is used. Reserved for future extensibility.

Child Namespace Inheritance

W3C Inheritance Semantics

Children in the same namespace as their parent automatically inherit the parent’s namespace without redeclaration. This follows W3C XML namespace specifications and prevents duplicate xmlns declarations.

Inheritance Rules:

  1. Priority 0 - Inherit parent’s default namespace (highest priority)

  2. Priority 0.5 - Use prefix hoisted on parent

  3. Priority 1-5 - Standard namespace resolution

Basic Inheritance Example

Example 1. Children inheriting parent’s default namespace
class VcardNamespace < Lutaml::Model::XmlNamespace
  uri "urn:ietf:params:xml:ns:vcard-4.0"
  prefix_default "vcard"
end

class Vcard < Lutaml::Model::Serializable
  attribute :version, :string
  attribute :fn, :string
  attribute :email, :string

  xml do
    element "vCard"
    namespace VcardNamespace

    map_element "version", to: :version
    map_element "fn", to: :fn
    map_element "email", to: :email
  end
end

vcard = Vcard.new(
  version: "4.0",
  fn: "Dr. John Doe",
  email: "john@example.com"
)

puts vcard.to_xml

Output (default namespace format):

<vCard xmlns="urn:ietf:params:xml:ns:vcard-4.0">
  <version>4.0</version>
  <fn>Dr. John Doe</fn>
  <email>john@example.com</email>
</vCard>
All child elements inherit parent’s default namespace—no xmlns redeclaration needed.

Output with prefix: true:

puts vcard.to_xml(prefix: true)
<vcard:vCard xmlns:vcard="urn:ietf:params:xml:ns:vcard-4.0">
  <vcard:version>4.0</vcard:version>
  <vcard:fn>Dr. John Doe</vcard:fn>
  <vcard:email>john@example.com</vcard:email>
</vcard:vCard>
All elements in same namespace use consistent vcard: prefix.

Type Namespaces with namespace_scope

Local Hoisting (Without namespace_scope)

When Type namespaces are used without namespace_scope, they are hoisted locally to the element where first used:

Example 2. Type namespace with local hoisting
class DcNamespace < Lutaml::Model::XmlNamespace
  uri "http://purl.org/dc/elements/1.1/"
  prefix_default "dc"
end

class DcTitleType < Lutaml::Model::Type::String
  xml_namespace DcNamespace
end

class Contact < Lutaml::Model::Serializable
  attribute :title, DcTitleType
  attribute :note, DcTitleType

  xml do
    element "contact"

    map_element "title", to: :title
    map_element "note", to: :note
  end
end

class Vcard < Lutaml::Model::Serializable
  attribute :contact, Contact

  xml do
    element "vCard"
    namespace VcardNamespace

    map_element "contact", to: :contact
  end
end

vcard = Vcard.new(
  contact: Contact.new(
    title: "Dr. John Doe",
    note: "Lead Researcher"
  )
)

puts vcard.to_xml

Output:

<vCard xmlns="urn:ietf:params:xml:ns:vcard-4.0">
  <contact xmlns:dc="http://purl.org/dc/elements/1.1/">
    <dc:title>Dr. John Doe</dc:title>
    <dc:note>Lead Researcher</dc:note>
  </contact>
</vCard>
DcNamespace declared once on <contact> (local hoisting), both children use dc: prefix.

Root Hoisting (With namespace_scope)

With namespace_scope, Type namespaces are hoisted to root, eliminating intermediate declarations:

Example 3. Type namespace with root hoisting via namespace_scope
class Vcard < Lutaml::Model::Serializable
  attribute :contact, Contact

  xml do
    element "vCard"
    namespace VcardNamespace

    # Hoist DcNamespace to root
    namespace_scope [
      { namespace: DcNamespace, declare: :always }
    ]

    map_element "contact", to: :contact
  end
end

vcard = Vcard.new(
  contact: Contact.new(
    title: "Dr. John Doe",
    note: "Lead Researcher"
  )
)

puts vcard.to_xml

Output:

<vCard xmlns="urn:ietf:params:xml:ns:vcard-4.0"
       xmlns:dc="http://purl.org/dc/elements/1.1/">
  <contact>
    <dc:title>Dr. John Doe</dc:title>
    <dc:note>Lead Researcher</dc:note>
  </contact>
</vCard>
DcNamespace hoisted to root. Children in <contact> recognize and use parent’s xmlns:dc declaration without redeclaration.

Multi-Namespace Documents

Complete Example: vCard with Metadata

This example demonstrates all namespace_scope features with four namespaces:

Example 4. Complete vCard with Dublin Core and DCTerms metadata
# 1. Define namespaces
class VcardNamespace < Lutaml::Model::XmlNamespace
  uri "urn:ietf:params:xml:ns:vcard-4.0"
  prefix_default "vcard"
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

# 2. Define Type classes
class DcTitleType < Lutaml::Model::Type::String
  xml_namespace DcNamespace
end

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

class DctermsCreatedType < Lutaml::Model::Type::DateTime
  xml_namespace DctermsNamespace
end

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

# 3. Define nested model
class Metadata < Lutaml::Model::Serializable
  attribute :title, DcTitleType
  attribute :creator, DcCreatorType
  attribute :created, DctermsCreatedType
  attribute :format, XsiTypeType

  xml do
    element "metadata"

    map_element "title", to: :title
    map_element "creator", to: :creator
    map_element "created", to: :created
    map_attribute "type", to: :format
  end
end

# 4. Define root model with namespace_scope
class Vcard < Lutaml::Model::Serializable
  attribute :version, :string
  attribute :fn, :string
  attribute :metadata, Metadata

  xml do
    element "vCard"
    namespace VcardNamespace

    # Consolidate all namespaces at root
    namespace_scope [
      { namespace: VcardNamespace, declare: :always },
      { namespace: DcNamespace, declare: :always },
      { namespace: DctermsNamespace, declare: :always },
      { namespace: XsiNamespace, declare: :always }
    ]

    map_element "version", to: :version
    map_element "fn", to: :fn
    map_element "metadata", to: :metadata
  end
end

# 5. Create and serialize
vcard = Vcard.new(
  version: "4.0",
  fn: "Dr. John Doe",
  metadata: Metadata.new(
    title: "Chief Technology Officer",
    creator: "John Doe",
    created: DateTime.parse("2024-06-01T12:00:00Z"),
    format: "vcard"
  )
)

puts vcard.to_xml

Output:

<vCard xmlns="urn:ietf:params:xml:ns:vcard-4.0"
       xmlns:dc="http://purl.org/dc/elements/1.1/"
       xmlns:dcterms="http://purl.org/dc/terms/"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <version>4.0</version>
  <fn>Dr. John Doe</fn>
  <metadata xsi:type="vcard">
    <dc:title>Chief Technology Officer</dc:title>
    <dc:creator>John Doe</dc:creator>
    <dcterms:created>2024-06-01T12:00:00+00:00</dcterms:created>
  </metadata>
</vCard>

Key features:

  • Four namespaces consolidated: All declared at root

  • Default namespace for vCard: <version> and <fn> use no prefix

  • Prefixed Type namespaces: dc:, dcterms:, xsi: use prefixes throughout

  • Child inheritance: Metadata children recognize parent’s declarations

  • No redeclaration: Each xmlns appears exactly once

  • W3C compliant: Full round-trip serialization support

Auto vs Always Declaration

Example 5. Comparing :auto vs :always declaration modes
class Vcard < Lutaml::Model::Serializable
  attribute :version, :string
  # No DcTitleType attributes used

  xml do
    element "vCard"
    namespace VcardNamespace

    # Try to hoist unused DcNamespace
    namespace_scope [
      { namespace: DcNamespace, declare: :auto }
    ]

    map_element "version", to: :version
  end
end

vcard = Vcard.new(version: "4.0")
puts vcard.to_xml

With :auto mode (namespace unused):

<vCard xmlns="urn:ietf:params:xml:ns:vcard-4.0">
  <version>4.0</version>
</vCard>
DcNamespace NOT declared because unused (:auto mode).

With :always mode:

namespace_scope [
  { namespace: DcNamespace, declare: :always }
]
<vCard xmlns="urn:ietf:params:xml:ns:vcard-4.0"
       xmlns:dc="http://purl.org/dc/elements/1.1/">
  <version>4.0</version>
</vCard>
DcNamespace IS declared even though unused (:always mode required by specification).

W3C Compliance

Lutaml::Model follows W3C XML Namespace specifications for child element qualification:

  1. Default namespace inheritance: Children in same namespace as parent inherit automatically

  2. Prefix consistency: Same namespace always uses same prefix throughout document

  3. No duplicate declarations: xmlns declared once per scope

  4. Explicit opt-out: Children in different namespace use xmlns="" when needed

This section implements the following W3C XML Schema Databinding Patterns:

element_form_default

The element_form_default setting in XmlNamespace classes controls child element namespace qualification, corresponding to W3C XML Schema’s elementFormDefault attribute.

:qualified

Children inherit parent namespace (W3C default for Lutaml::Model)

:unqualified (explicitly set)

Children in blank namespace (requires xmlns="" in output)

W3C Pattern: UnqualifiedLocalElements

XSD Schema
<xs:schema targetNamespace="http://example.com/ns"
           elementFormDefault="unqualified">
  <xs:element name="unqualifiedLocalElements" type="xs:string" />
</xs:schema>
Valid Instance
<unqualifiedLocalElements>some data</unqualifiedLocalElements>

W3C Pattern: ElementFormUnqualified

XSD Schema
<xs:element name="elementFormUnqualified" type="ex:ElementFormUnqualified" />
<xs:complexType name="ElementFormUnqualified">
  <xs:sequence>
    <xs:element name="element" type="xs:string" form="unqualified" />
  </xs:sequence>
</xs:complexType>
Valid Instance
<ex:elementFormUnqualified>
    <element>string</element>
</ex:elementFormUnqualified>
Explicit Setting Required

For predictable behavior, always explicitly set element_form_default to match your XSD’s elementFormDefault attribute.

When element_form_default :unqualified is explicitly set:

  • Children are placed in the blank namespace

  • No namespace prefix on child elements

  • Appropriate when XSD declares elementFormDefault="unqualified"

When element_form_default is not set (relies on default):

  • May not produce predictable unqualified behavior

  • Context-dependent inheritance may occur

  • Not recommended for XSD-compliant serialization

Example 6. element_form_default behavior (explicitly set to :unqualified)
class ParentNamespace < Lutaml::Model::XmlNamespace
  uri "http://example.com/parent"
  prefix_default "parent"
  element_form_default :unqualified  # EXPLICITLY SET
end

class Parent < Lutaml::Model::Serializable
  attribute :child_value, :string

  xml do
    element "parent"
    namespace ParentNamespace

    map_element "child", to: :child_value
  end
end

parent = Parent.new(child_value: "value")
puts parent.to_xml(prefix: true)

Output:

<parent:parent xmlns:parent="http://example.com/parent">
  <child xmlns="">value</child>
</parent:parent>
Child uses xmlns="" to explicitly declare blank namespace (opt out of parent’s namespace).

Format Preservation

Lutaml::Model preserves the exact namespace format (default vs prefix) from input XML during round-trip serialization.

Declaration Plan Modes

The import_declaration_plan: option on from_xml controls how namespace declarations are captured for round-trip fidelity:

Mode Default? Description

:lazy

Yes

Stores a lightweight reference to the parsed element tree. The declaration plan is built on first to_xml call, then cached. Best balance of fast parsing and round-trip fidelity.

:eager

No

Builds the declaration plan during from_xml. Full round-trip fidelity with slightly slower parsing.

:skip

No

Never builds the declaration plan. Fastest parsing, but namespace format (prefix vs default) is not preserved during to_xml.

# Default (:lazy) — recommended for most use cases
model = MyClass.from_xml(xml)
model.to_xml  # Namespace format preserved

# Eager — for read-once-serialize-many workflows
model = MyClass.from_xml(xml, import_declaration_plan: :eager)

# Skip — for read-only parsing where serialization won't happen
model = MyClass.from_xml(xml, import_declaration_plan: :skip)

Format Preservation Examples

Example 7. Format preservation example

Input XML (default format):

<vCard xmlns="urn:ietf:params:xml:ns:vcard-4.0">
  <version>4.0</version>
</vCard>
vcard = Vcard.from_xml(xml)
vcard.to_xml  # Preserves default format

Output (default format preserved):

<vCard xmlns="urn:ietf:params:xml:ns:vcard-4.0">
  <version>4.0</version>
</vCard>

Input XML (prefix format):

<vcard:vCard xmlns:vcard="urn:ietf:params:xml:ns:vcard-4.0">
  <vcard:version>4.0</vcard:version>
</vcard:vCard>
vcard = Vcard.from_xml(xml)
vcard.to_xml  # Preserves prefix format

Output (prefix format preserved):

<vcard:vCard xmlns:vcard="urn:ietf:params:xml:ns:vcard-4.0">
  <vcard:version>4.0</vcard:version>
</vcard:vCard>

Namespace Identifiers

Per the W3C XML Namespaces specification, namespace names are URI references (RFC 3986).

Both URLs and URNs are valid namespace identifiers:

The W3C spec notes that conforming processors are not required to validate that namespace names are syntactically valid URI references. Lutaml::Model uses lenient validation to also accept non-standard identifiers (e.g., Formal Public Identifiers like -//OASIS//DTD XML Exchange Table Model//EN).

Best Practices

When to Use namespace_scope

Use namespace_scope when:

  • Multiple child elements share same namespaces

  • Document structure benefits from consolidated declarations

  • Format specification requires specific namespaces at root (e.g., OOXML)

  • Cleaner XML readability is important

Don’t use namespace_scope when:

  • Namespace used in limited scope only

  • Default (local) declaration is sufficient

  • Simplest possible XML output desired

Choosing Declaration Modes

Use :auto mode when:

  • Standard W3C behavior desired

  • Minimize unnecessary declarations

  • Generate clean, minimal XML

  • Most common case

Use :always mode when:

  • Schema requires specific namespace declarations

  • External tools validate namespace presence

  • Format specifications mandate unused namespaces (e.g., Office Open XML with xmlns:vt)

Use :never mode when:

  • Explicitly prevent namespace usage

  • Catch errors during development

  • Reserved for future features

Type Namespaces

Type-level namespaces (via xml_namespace on custom types) are particularly useful for:

  • Reusable types belonging to specific namespaces (Dublin Core, custom XSD types)

  • Multi-namespace document structures (Office Open XML, metadata)

  • Automatic namespace application

  • W3C-compliant round-trip serialization

Example 8. Using Type namespaces
class DcTitleType < Lutaml::Model::Type::String
  xml_namespace DcNamespace
  xsd_type 'titleType'
end

class Document < Lutaml::Model::Serializable
  attribute :title, DcTitleType  # Automatically uses DcNamespace

  xml do
    element "document"
    namespace DocumentNamespace

    map_element "title", to: :title  # Becomes <dc:title>
  end
end

Common Patterns

Pattern 1: Single Root Namespace

Simplest case—all elements in one namespace:

class Document < Lutaml::Model::Serializable
  xml do
    element "document"
    namespace DocumentNamespace
    # No namespace_scope needed
  end
end

Pattern 2: Root + Type Namespaces (Local Hoisting)

Root namespace for structure, Type namespaces for specific attributes:

class Document < Lutaml::Model::Serializable
  attribute :title, DcTitleType  # DcNamespace
  attribute :created, DctermsCreatedType  # DctermsNamespace

  xml do
    element "document"
    namespace DocumentNamespace
    # No namespace_scope - Type namespaces hoist locally
  end
end

Pattern 3: Full Consolidation with namespace_scope

All namespaces declared at root for clean, organized XML:

class Document < Lutaml::Model::Serializable
  attribute :title, DcTitleType
  attribute :created, DctermsCreatedType

  xml do
    element "document"
    namespace DocumentNamespace

    # Hoist all Type namespaces to root
    namespace_scope [
      { namespace: DcNamespace, declare: :always },
      { namespace: DctermsNamespace, declare: :always }
    ]
  end
end

Pattern 4: Mixed Declaration Modes

Some namespaces always declared, others conditional:

xml do
  namespace RootNamespace

  namespace_scope [
    { namespace: CommonNamespace, declare: :always },   # Always present
    { namespace: OptionalNamespace, declare: :auto }    # Only if used
  ]
end

Troubleshooting

Duplicate xmlns Declarations

Problem: Same xmlns appears multiple times

<root xmlns:dc="...">
  <child xmlns:dc="...">  <!-- Duplicate! -->
  </child>
</root>

Solution: Ensure child inherits parent’s declaration:

# Correct: namespace_scope hoists to root
xml do
  namespace_scope [DcNamespace]
end

Missing Namespace Prefixes

Problem: Elements missing expected prefixes

<title>...</title>  <!-- Expected <dc:title> -->

Solution: Verify Type namespace declaration:

class DcTitleType < Lutaml::Model::Type::String
  xml_namespace DcNamespace  # Must be declared
end

Unnecessary xmlns="" Declarations

Problem: Blank xmlns on children

<parent xmlns="...">
  <child xmlns="">...</child>  <!-- Unnecessary if same namespace -->
</parent>

Solution: Verify child is in same namespace as parent. xmlns="" only needed when child is in different namespace or blank namespace.

Redundant xmlns Declarations on Nested Elements

Problem: Nested elements re-declare the same namespace

<Unit xmlns="http://example.com/units">
  <RootUnits>
    <EnumeratedRootUnit xmlns="http://example.com/units"/>  <!-- Redundant! -->
  </RootUnits>
</Unit>

Solution: This was a bug fixed in version 0.7.x. Child elements in the same namespace as their parent now correctly inherit the namespace without re-declaration.

<Unit xmlns="http://example.com/units">
  <RootUnits>
    <EnumeratedRootUnit/>  <!-- Correctly inherits parent namespace -->
  </RootUnits>
</Unit>

This follows W3C XML Namespaces 1.0 §6.2 which states that child elements inherit the default namespace from their parent.

Adapter Compatibility

All four XML adapters (Nokogiri, Ox, Oga, REXML) now support the same namespace features:

Feature Nokogiri Ox Oga REXML

Default namespace format

✓

✓

✓

✓

Prefixed namespace format

✓

✓

✓

✓

Namespace inheritance

✓

✓

✓

✓

Custom prefix strings (prefix: "custom")

✓

✓

✓

✓

Round-trip namespace preservation

✓

✓

✓

✓

All adapters now produce identical namespace output. The Oga adapter namespace issues that existed in earlier versions have been resolved.

Architecture Notes

For developers working on Lutaml::Model internals:

  • DeclarationPlanner: Makes ALL namespace decisions (where to declare, what format)

  • Adapters: Only apply decisions (Dumb Adapter Principle)

  • Three Phases: Collect → Plan → Render

  • XmlDataModel: Content layer (WHAT to serialize)

  • DeclarationPlan: Declaration layer (WHERE and HOW to declare namespaces)

See Memory Bank for detailed architecture documentation.