General

This guide explains how Lutaml::Model handles XML namespace declarations (xmlns and xmlns:prefix) in serialized XML output.

Understanding namespace declaration placement is crucial for:

  • Generating clean, W3C-compliant XML

  • Controlling xmlns declaration verbosity

  • Meeting specific schema requirements

  • Ensuring interoperability with external systems

Declaration strategies

Two valid approaches

Lutaml::Model implements local xmlns declarations as the default strategy, following the W3C minimal-subtree principle. However, both approaches below are W3C-compliant:

Local declarations (Lutaml::Model default)

Namespaces are declared on the first element that uses them

<root xmlns:foo="...">
  <child>
    <item xmlns:bar="..."/>
  </child>
</root>

Advantages:

  • Minimal scope (namespace only valid where needed)

  • Cleaner nested structures

  • Clear ownership of namespace

  • Follows minimal-subtree principle

Hoisted declarations (alternative, not default)

All namespaces declared at document root

<root xmlns:foo="..." xmlns:bar="...">
  <child>
    <item/>
  </child>
</root>

Advantages:

  • Single declaration location

  • Slightly smaller file size (fewer xmlns attributes)

  • Traditional style preferred by some tools

Why local declarations?

Lutaml::Model defaults to local declarations because:

  1. W3C Minimal-Subtree Principle: Namespaces should be declared in the smallest subtree that needs them

  2. Scope clarity: Easy to see which elements use which namespaces

  3. Modularity: Subtrees can be extracted without losing namespace context

  4. Cleaner structure: No namespace pollution at root for deeply nested namespaces

The namespace_scope directive

What it does (and doesn’t do)

IMPORTANT: namespace_scope makes namespaces ELIGIBLE for declaration at the root element. It does NOT automatically hoist all declarations.

namespace_scope behavior with declare: modes:

:auto (default)

Namespace declared at root ONLY IF actually used in elements/attributes

:always

Namespace declared at root even if unused (for schema compliance)

:never

Namespace never declared (error if used)

Eligibility vs. automatic hoisting

The key concept: namespace_scope grants eligibility, not certainty.

Example 1. namespace_scope with :auto mode (default)
class Vcard < Lutaml::Model::Serializable
  xml do
    element "vCard"
    namespace VcardNamespace
    namespace_scope [VcardNamespace, DcNamespace, DctermsNamespace]
    # namespace_scope makes these three namespaces ELIGIBLE for root declaration

    map_element "title", to: :title
    map_element "created", to: :created
  end
end

If title and created use DcNamespace and DctermsNamespace:

<vCard xmlns="..." xmlns:dc="..." xmlns:dcterms="...">
  <dc:title>...</dc:title>
  <dcterms:created>...</dcterms:created>
</vCard>

If title and created DON’T use those namespaces:

<vCard xmlns="...">
  <title>...</title>
  <created>...</created>
</vCard>
With :auto mode, unused namespaces in namespace_scope are NOT declared. They are only declared if actually used.

Forcing declarations with :always

Use declare: :always for namespaces that MUST appear regardless of usage:

Example 2. Forcing namespace declarations
class Properties < Lutaml::Model::Serializable
  xml do
    element "Properties"
    namespace AppNamespace

    namespace_scope [
      { namespace: VtNamespace, declare: :always }  # Force VtNamespace
    ]

    map_element "Template", to: :template
  end
end

Output (VtNamespace declared even though unused):

<Properties xmlns="..."
            xmlns:vt="...">
  <Template>Normal.dotm</Template>
</Properties>

This is required by some formats like Office Open XML where xmlns:vt must always be present.

Collection prefix behavior

Parent-child prefix inheritance

Collection <item> elements follow their parent’s prefix style:

Parent WITH prefix → Items WITH prefix
<first:collection xmlns:first="...">
  <first:item>
    <name>Value</name>
  </first:item>
</first:collection>
Parent WITHOUT prefix → Items WITHOUT prefix
<collection xmlns="...">
  <item>
    <name>Value</name>
  </item>
</collection>

Grandchildren inherit context

Elements within collection items inherit the namespace context and do NOT repeat the prefix:

Example 3. Grandchildren don’t repeat prefixes
<first:collection xmlns:first="http://example.com/first">
  <first:item>
    <name>John</name>         <!-- No prefix, inherits default ns -->
    <address>
      <street>123 Main</street>  <!-- No prefix, inherits default ns -->
    </address>
  </first:item>
</first:collection>

The <name> and <street> elements don’t need first: prefix because they are in the first namespace context established by <first:item>.

Understanding declaration planning

The declaration algorithm

When serializing, Lutaml::Model uses this algorithm to decide where to declare namespaces:

  1. Collect used namespaces: Scan all elements/attributes to determine which namespaces are actually used

  2. Check namespace_scope eligibility: For each used namespace, check if it’s in the namespace_scope list

  3. Declare at root if eligible: Namespaces in namespace_scope with :auto or :always mode are declared at root (if :auto, only when used)

  4. Declare locally otherwise: Namespaces not in namespace_scope are declared on the first element that uses them

  5. Never redeclare: Once declared, namespace prefixes are reused without redeclaration

Visual examples

Example 4. Example 1: No namespace_scope (all local)
class Parent < Lutaml::Model::Serializable
  xml do
    element "Parent"
    namespace NamespaceA
    # NO namespace_scope directive

    map_element "Child", to: :child
  end
end

Output:

<Parent xmlns="http://a.com">
  <Child xmlns="http://b.com">...</Child>
</Parent>

Each namespace declared where first used.

Example 5. Example 2: With namespace_scope (eligible for root)
class Parent < Lutaml::Model::Serializable
  xml do
    element "Parent"
    namespace NamespaceA
    namespace_scope [NamespaceA, NamespaceB]  # Both eligible

    map_element "Child", to: :child
  end
end

Output:

<Parent xmlns="http://a.com" xmlns:b="http://b.com">
  <b:Child>...</b:Child>
</Parent>

Both namespaces declared at root because they’re in namespace_scope and both are used.

Example 6. Example 3: namespace_scope with unused namespace
class Parent < Lutaml::Model::Serializable
  xml do
    element "Parent"
    namespace NamespaceA
    namespace_scope [NamespaceA, NamespaceB, NamespaceC]  # C is unused

    map_element "Child", to: :child  # Uses NamespaceB
  end
end

Output with :auto mode (default):

<Parent xmlns="http://a.com" xmlns:b="http://b.com">
  <b:Child>...</b:Child>
</Parent>

NamespaceC is NOT declared because it’s not used (:auto mode).

Output with declare: :always for NamespaceC:

<Parent xmlns="http://a.com"
        xmlns:b="http://b.com"
        xmlns:c="http://c.com">
  <b:Child>...</b:Child>
</Parent>

NamespaceC IS declared even though unused (:always mode).

Common patterns

Pattern 1: Single namespace document

class Document < Lutaml::Model::Serializable
  xml do
    element "document"
    namespace DocNamespace

    map_element "title", to: :title
    map_element "content", to: :content
  end
end

Output (default namespace, no prefixes):

<document xmlns="http://example.com/doc">
  <title>My Document</title>
  <content>Text content</content>
</document>

Pattern 2: Multi-namespace with consolidation

class Document < Lutaml::Model::Serializable
  xml do
    element "document"
    namespace DocNamespace
    namespace_scope [DocNamespace, MetaNamespace, AuthorNamespace]

    map_element "title", to: :title
    map_element "meta", to: :meta
    map_element "author", to: :author
  end
end

Output (all namespaces at root if used):

<document xmlns="http://example.com/doc"
          xmlns:meta="http://example.com/meta"
          xmlns:author="http://example.com/author">
  <title>My Document</title>
  <meta:keywords>...</meta:keywords>
  <author:name>...</author:name>
</document>

Pattern 3: Mixed local and scoped namespaces

class Document < Lutaml::Model::Serializable
  xml do
    element "document"
    namespace DocNamespace
    namespace_scope [DocNamespace, MetaNamespace]  # Only these two at root

    map_element "title", to: :title
    map_element "meta", to: :meta      # Uses MetaNamespace
    map_element "internal", to: :internal  # Uses InternalNamespace (not in scope)
  end
end

Output (meta at root, internal local):

<document xmlns="http://example.com/doc"
          xmlns:meta="http://example.com/meta">
  <title>My Document</title>
  <meta:keywords>...</meta:keywords>
  <internal xmlns="http://example.com/internal">...</internal>
</document>

Pattern 4: Collection with different namespace

class Wrapper < Lutaml::Model::Serializable
  attribute :items, Item, collection: true

  xml do
    element "wrapper"
    namespace WrapperNamespace

    map_element "item", to: :items  # Item class uses ItemNamespace
  end
end

Output (wrapper in one ns, items in another):

<wr:wrapper xmlns:wr="http://wrapper.com">
  <item xmlns="http://item.com">
    <name>Value</name>
  </item>
</wr:wrapper>

Item elements get their own namespace declaration on first usage.

Best practices

When to use namespace_scope

Use namespace_scope when:

  • You want cleaner XML with fewer xmlns declarations

  • Multiple child elements use the same external namespace

  • Schema requires specific namespace declarations at root

  • Generating documents for legacy tools expecting hoisted declarations

Don’t use namespace_scope when:

  • Single namespace document (no benefit)

  • Namespaces only used deep in the tree (adds clutter at root)

  • Different subtrees use different namespaces (defeats consolidation)

Choosing declaration mode

Use :auto (default) when:

  • Standard W3C behavior desired

  • Clean, minimal XML preferred

  • Namespaces should only appear when actually used

Use :always when:

  • External schema validation requires namespace presence

  • Tool compatibility demands specific xmlns declarations

  • Format specification mandates unused namespaces (e.g., Office Open XML)

Prefix vs. default namespace

Use default namespace (xmlns="…​") when:

  • Single dominant namespace in document

  • Cleaner, more readable XML desired

  • No prefix preferred for main elements

Use prefixed namespace (xmlns:prefix="…​") when:

  • Multiple namespaces at same level

  • Need to distinguish element sources clearly

  • Attributes require namespace (attributes MUST use prefix)

  • Legacy tools expect prefixed format

Known limitations

Type::Value namespace feature

Status: Not yet implemented

Custom type classes (inheriting from Lutaml::Model::Type::Value) cannot currently declare namespaces via xml_namespace for automatic serialization.

Example 7. Type namespace feature (future enhancement)
# This feature does NOT work yet:
class DcTitleType < Lutaml::Model::Type::String
  xml_namespace DcNamespace  # Not yet supported for Type::Value classes
end

class Vcard < Lutaml::Model::Serializable
  attribute :title, DcTitleType

  xml do
    element "vCard"
    map_element "title", to: :title
  end
end

# Current output:
# <vCard><title>Dr. John Doe</title></vCard>

# Expected future output:
# <vCard xmlns:dc="..."><dc:title>Dr. John Doe</dc:title></vCard>

Workaround: Use Lutaml::Model::Serializable classes instead of Type::Value classes for attributes that need namespace-qualified serialization.

Troubleshooting

Namespace not declared at root

Problem: Used namespace appears locally instead of at root

Cause: Namespace not in namespace_scope list

Solution: Add to namespace_scope:

namespace_scope [MyNamespace, OtherNamespace]

Namespace declared but unused

Problem: Namespace appears at root even though not used

Cause: Using declare: :always mode

Solution: Change to :auto or remove from namespace_scope:

namespace_scope [
  { namespace: MyNamespace, declare: :auto }  # Change to :auto
]

xmlns="" appearing unexpectedly

Problem: Child elements get xmlns="" attribute

Cause: Child model has no namespace but parent uses default namespace

Solution: Either:

  1. Give child model a namespace

  2. Use prefixed namespace for parent (not default)

  3. Accept xmlns="" as correct W3C behavior (removes parent’s default namespace)

Collection items missing prefix

Problem: Expected <first:item> but got <item>

Cause: Parent element not using prefix (using default namespace instead)

Solution: Use prefix: true when serializing:

model.to_xml(prefix: true)

W3C compliance notes

Namespace URI is identity

Remember: Namespace URI determines identity, NOT the prefix.

These are semantically identical:

<!-- All three are equivalent: -->
<Ceramic xmlns="http://ex.com/cer">...</Ceramic>
<cer:Ceramic xmlns:cer="http://ex.com/cer">...</cer:Ceramic>
<pottery:Ceramic xmlns:pottery="http://ex.com/cer">...</pottery:Ceramic>

Lutaml::Model correctly handles all three formats during parsing.

Unprefixed attributes

Per W3C XML Namespace specification: Unprefixed attributes have NO namespace.

Only attributes with explicit prefixes belong to namespaces:

<Ceramic id="C001"                    <!-- id has NO namespace -->
         xmlns:cer="http://ex.com">
  <cer:Type>Porcelain</cer:Type>
</Ceramic>

Minimal-subtree principle

W3C XML Namespace specification recommends declaring namespaces in the minimal subtree that needs them. This is why Lutaml::Model defaults to local declarations instead of hoisting everything to the root.

Summary

Key points to remember:

  1. Local declarations are the default (minimal-subtree principle)

  2. namespace_scope grants ELIGIBILITY, not automatic hoisting

  3. Both local and hoisted are W3C-compliant, Lutaml chooses local

  4. Collection items inherit parent’s prefix style

  5. Grandchildren inherit context (no repeated prefixes)

  6. Type::Value namespace is a future enhancement (not yet implemented)

For detailed namespace configuration options, see XML Namespaces Guide.

For architectural details, see Three-Phase Namespace Architecture.