General

XML namespaces provide a way to qualify element and attribute names to avoid conflicts. Lutaml::Model provides comprehensive namespace support following W3C specifications.

Namespace prefix independence

General

Lutaml::Model implements W3C-compliant XML namespace processing where namespace URIs, not prefixes, determine element identity. This means that models will correctly parse XML documents regardless of which prefix (or no prefix) is used, as long as the namespace URI matches.

This W3C-compliant behavior ensures maximum interoperability when consuming XML from different sources.

The namespace URI determines identity, not the prefix.

Supported prefix formats

When a model is defined with a namespace, it will successfully parse XML using any of these formats:

Default namespace (unprefixed)

xmlns="http://example.com/ns"

Prefixed namespace with model’s default prefix

xmlns:cer="http://example.com/ns"

Prefixed namespace with ANY arbitrary prefix

xmlns:pottery="http://example.com/ns"

All three formats are semantically identical according to W3C XML Namespaces specification.

When there is no default namespace declared, unprefixed elements are treated as having the "blank" namespace, i.e. xmlns="". It is possible to force elements to have a blank namespace using #to_xml(prefix: "") or #to_xml(prefix: nil).

Example 1. Parsing XML with different prefix formats
class CeramicNamespace < Lutaml::Model::XmlNamespace
  uri 'http://example.com/ceramic'
  prefix_default 'cer'
end

class Ceramic < Lutaml::Model::Serializable
  attribute :type, :string

  xml do
    element 'Ceramic'
    namespace CeramicNamespace
    map_element 'Type', to: :type
  end
end

# All these XML formats parse successfully to the same result:

# Format 1: Default namespace
xml1 = '<Ceramic xmlns="http://example.com/ceramic"><Type>Porcelain</Type></Ceramic>'
Ceramic.from_xml(xml1).type  # => "Porcelain"

# Format 2: Model's defined prefix
xml2 = '<cer:Ceramic xmlns:cer="http://example.com/ceramic"><cer:Type>Porcelain</cer:Type></cer:Ceramic>'
Ceramic.from_xml(xml2).type  # => "Porcelain"

# Format 3: Arbitrary prefix
xml3 = '<pottery:Ceramic xmlns:pottery="http://example.com/ceramic"><pottery:Type>Porcelain</pottery:Type></pottery:Ceramic>'
Ceramic.from_xml(xml3).type  # => "Porcelain"

All three examples produce identical Ceramic objects. The namespace URI http://example.com/ceramic matches, which is what matters according to W3C specifications.

Nested models with different namespaces

Prefix independence extends to nested models where parent and child use different namespaces:

Example 2. Nested models with arbitrary prefixes
class GlazeNamespace < Lutaml::Model::XmlNamespace
  uri 'http://example.com/glaze'
  prefix_default 'glz'
end

class Glaze < Lutaml::Model::Serializable
  attribute :color, :string

  xml do
    element 'Glaze'
    namespace GlazeNamespace
    map_element 'Color', to: :color
  end
end

class Ceramic < Lutaml::Model::Serializable
  attribute :type, :string
  attribute :glaze, Glaze

  xml do
    element 'Ceramic'
    namespace CeramicNamespace
    map_element 'Type', to: :type
    map_element 'Glaze', to: :glaze
  end
end

# Parent and child can use any prefixes as long as URIs match:
xml = <<~XML
  <x:Ceramic xmlns:x="http://example.com/ceramic"
             xmlns:y="http://example.com/glaze">
    <x:Type>Porcelain</x:Type>
    <y:Glaze>
      <y:Color>Clear</y:Color>
    </y:Glaze>
  </x:Ceramic>
XML

result = Ceramic.from_xml(xml)
result.type          # => "Porcelain"
result.glaze.color   # => "Clear"

Type-level namespace prefix independence

When attribute types declare their own namespaces via xml_namespace, those types also support prefix-independent parsing:

Example 3. Type namespace with arbitrary prefixes
class ColorNamespace < Lutaml::Model::XmlNamespace
  uri 'http://example.com/color'
  prefix_default 'clr'
end

class ColorType < Lutaml::Model::Type::String
  xml_namespace ColorNamespace
end

class Ceramic < Lutaml::Model::Serializable
  attribute :type, :string
  attribute :color, ColorType

  xml do
    element 'Ceramic'
    namespace CeramicNamespace
    map_element 'Type', to: :type
    map_element 'Color', to: :color
  end
end

# Color element can use any prefix for the color namespace:
xml = <<~XML
  <Ceramic xmlns="http://example.com/ceramic"
           xmlns:c="http://example.com/color">
    <Type>Porcelain</Type>
    <c:Color>Navy Blue</c:Color>
  </Ceramic>
XML

result = Ceramic.from_xml(xml)
result.color  # => "Navy Blue"

Round-trip serialization

Models support round-trip serialization between different namespace formats. You can parse XML with any prefix format and serialize to either default or prefixed format:

Example 4. Round-trip with different formats
# Parse XML with arbitrary prefix
xml_arbitrary = '<pottery:Ceramic xmlns:pottery="http://example.com/ceramic"><pottery:Type>Porcelain</pottery:Type></pottery:Ceramic>'

ceramic = Ceramic.from_xml(xml_arbitrary)

# Serialize to default namespace format
ceramic.to_xml
# => '<Ceramic xmlns="http://example.com/ceramic"><Type>Porcelain</Type></Ceramic>'

# Serialize to prefixed format with model's defined prefix
ceramic.to_xml(prefix: true)
# => '<cer:Ceramic xmlns:cer="http://example.com/ceramic"><cer:Type>Porcelain</cer:Type></cer:Ceramic>'

Both serialized formats can be parsed back successfully, demonstrating full prefix independence.

Namespace Format Preservation (Round Trip)

Automatic Preservation

Lutaml::Model automatically preserves the namespace format from input XML:

Example 5. Default format preservation
# Parse XML with default format
xml = '<Model xmlns="http://example.com"><field>value</field></Model>'
model = Model.from_xml(xml)

# Serialize preserves default format
model.to_xml
# => '<Model xmlns="http://example.com">...</Model>'
Example 6. Prefix format preservation
# Parse XML with prefix format
xml = '<ex:Model xmlns:ex="http://example.com"><ex:field>value</ex:field></ex:Model>'
model = Model.from_xml(xml)

# Serialize preserves prefix format
model.to_xml
# => '<ex:Model xmlns:ex="http://example.com">...</ex:Model>'

Implementation Architecture

The format preservation uses a four-phase process:

  1. Phase 1: Input Format Capture - During XML parsing, namespace declarations are extracted with their format (:default or :prefix)

  2. Phase 2: Metadata Storage - Format information is stored on the model instance as @__input_namespaces

  3. Phase 3: Declaration Planning - The DeclarationPlanner receives input namespaces and matches by URI to preserve format

  4. Phase 4: Serialization - The adapter applies the planned declarations with preserved format

Programmatic Models

For models created programmatically (not from XML), the format is determined by:

  1. Explicit prefix option: model.to_xml(prefix: true/false)

  2. Namespace configuration: prefix_default in XmlNamespace class

  3. W3C rules: Attributes in element’s namespace require prefix format

  4. Default: Prefer default format (cleaner output)

Format Priority

When multiple format sources exist, priority is:

  1. Input format (from parsed XML) - Highest priority, always preserved

  2. Explicit option (prefix: true/false)

  3. W3C compliance (e.g., attributes require prefix)

  4. Namespace configuration (prefix_default)

  5. Default format - Lowest priority

Adapter Support

All three XML adapters support format preservation:

  • Nokogiri - Full support ✅

  • Ox - Full support ✅

  • Oga - Full support ✅

Elements in blank namespace

When you need an element to have NO namespace (not even inherit parent namespace), create a child model class without a namespace declaration.

Example 7. Using child model class for elements in blank namespace
# Child model with no namespace
class Note < Lutaml::Model::Serializable
  attribute :value, :string

  xml do
    element "Note"
    # No namespace declaration = blank namespace
    map_content to: :value
  end
end

class MixedModel < Lutaml::Model::Serializable
  attribute :type, :string
  attribute :note, Note

  xml do
    element "Ceramic"
    namespace CeramicNamespace
    map_element "Type", to: :type
    map_element "Note", to: :note  # Uses Note's namespace (blank)
  end
end

When parent uses prefixed namespace (recommended):

Unprefixed child elements naturally have no namespace:

<cer:Ceramic xmlns:cer="http://example.com/ceramic">
  <cer:Type>Porcelain</cer:Type>
  <Note>This element has no namespace</Note>
</cer:Ceramic>

When parent uses default namespace:

Must use xmlns="" to explicitly remove the namespace:

<Ceramic xmlns="http://example.com/ceramic">
  <Type>Porcelain</Type>
  <Note xmlns="">This element has no namespace</Note>
</Ceramic>

Per W3C XML Namespace specification, unprefixed elements normally inherit their parent’s default namespace. The xmlns="" declaration explicitly removes that inheritance.

Parsing support: All three adapters (Nokogiri, Ox, Oga) correctly handle both patterns.

Namespace assignment priority

Namespace resolution follows a strict priority system when multiple namespace sources are available:

Table 1. Namespace priority (highest to lowest)
Priority Source Example

1 (HIGHEST)

Model-level namespace

attribute :attr, ChildModel where ChildModel has namespace

2

Type level

attribute :attr, CustomType where CustomType has xml_namespace

3 (LOWEST)

Context level (schema defaults)

element_form_default: :qualified in parent namespace

Example 8. Priority in action
class ItemNamespace < Lutaml::Model::XmlNamespace
  uri "http://example.com/items"
  prefix_default "item"
  element_form_default :qualified
end

class BlankNamespace < Lutaml::Model::XmlNamespace
  uri ""  # Blank namespace
end

class PriceType < Lutaml::Model::Type::String
  xml_namespace PriceNamespace  # Type-level namespace
end

# Child model in blank namespace
class Name < Lutaml::Model::Serializable
  attribute :value, :string

  xml do
    element "name"
    namespace BlankNamespace  # Model-level namespace (blank)
    map_content to: :value
  end
end

class Product < Lutaml::Model::Serializable
  attribute :name, Name
  attribute :price, PriceType
  attribute :note, :string

  xml do
    element "product"
    namespace ItemNamespace

    # Priority 1: Model-level namespace (blank)
    map_element "name", to: :name

    # Priority 2: Type-level namespace
    map_element "price", to: :price

    # Priority 3: Context from schema (qualified by default)
    map_element "note", to: :note
  end
end

Result with prefix: true:

<item:product xmlns:item="http://example.com/items"
              xmlns:price="http://example.com/prices">
  <name>Widget</name>                    <!-- Priority 1: model namespace (blank) -->
  <price:price>19.99</price:price>       <!-- Priority 2: type namespace -->
  <item:note>Description</item:note>     <!-- Priority 3: schema qualified -->
</item:product>

Implementation note

This prefix-independent behavior is achieved through namespace URI matching during XML parsing. The implementation compares namespace URIs (not prefix strings) when matching elements to model attributes, ensuring W3C compliance and maximum interoperability.

W3C Standard Namespaces

The xml Namespace

The xml namespace (http://www.w3.org/XML/1998/namespace) has special status in W3C XML specifications:

  • The prefix xml is reserved and implicitly bound to this URI

  • MUST NOT be declared with xmlns:xml="…​"

  • Always available for xml:lang, xml:space, xml:base, xml:id attributes

Lutaml::Model correctly handles this W3C requirement - the xml namespace is never declared in serialized XML.

Example 9. Using W3C xml namespace attributes
class XmlNamespace < Lutaml::Model::XmlNamespace
  uri "http://www.w3.org/XML/1998/namespace"
  prefix_default "xml"
end

class XmlLangType < Lutaml::Model::Type::String
  xml_namespace XmlNamespace
end

class XmlSpaceType < Lutaml::Model::Type::String
  xml_namespace XmlNamespace
end

class Document < Lutaml::Model::Serializable
  attribute :lang, XmlLangType
  attribute :space, XmlSpaceType
  attribute :content, :string

  xml do
    root "doc"
    map_attribute "lang", to: :lang
    map_attribute "space", to: :space
    map_content to: :content
  end
end

doc = Document.new(lang: "en", space: "preserve", content: "  Text  ")
puts doc.to_xml

Output:

<doc xml:lang="en" xml:space="preserve">  Text  </doc>
No xmlns:xml declaration appears - the xml namespace is implicitly bound per W3C specification.
Table 2. Common xml namespace attributes
Attribute Purpose

xml:lang

Specifies the natural language of content (RFC 5646 language tags)

xml:space

Controls whitespace handling (default or preserve)

xml:id

Provides unique identifiers (W3C XML ID)

xml:base

Sets base URI for relative URI resolution

XML namespace

General

An XML namespace is represented in Lutaml::Model as an inherited class from XmlNamespace. Model and value classes can declare their namespace using such XML namespace class.

The XmlNamespace class provides a declarative way to define XML namespace metadata following W3C XML Namespace and XSD specifications.

W3C XML namespace types

Lutaml::Model provides pre-defined type classes for W3C xml namespace attributes with built-in validation and serialization support.

Table 3. Available W3C XML attribute types
Type Class Purpose Validation

Lutaml::Model::Xml::W3c::XmlLangType

Language identification (RFC 5646)

Accepts any valid language tag

Lutaml::Model::Xml::W3c::XmlSpaceType

Whitespace handling control

Only "default" or "preserve"

Lutaml::Model::Xml::W3c::XmlBaseType

Base URI for relative references

Accepts any valid URI string

Lutaml::Model::Xml::W3c::XmlIdType

Unique identifier (XML ID type)

Must be valid NCName

Example 10. Using W3C XML attribute types with validation
class Article < Lutaml::Model::Serializable
  attribute :lang, Lutaml::Model::Xml::W3c::XmlLangType
  attribute :space, Lutaml::Model::Xml::W3c::XmlSpaceType
  attribute :id, Lutaml::Model::Xml::W3c::XmlIdType
  attribute :base, Lutaml::Model::Xml::W3c::XmlBaseType
  attribute :title, :string
  attribute :content, :string

  xml do
    root "article"
    map_attribute "lang", to: :lang
    map_attribute "space", to: :space
    map_attribute "id", to: :id
    map_attribute "base", to: :base
    map_element "title", to: :title
    map_content to: :content
  end
end

# Valid usage
article = Article.new(
  lang: "en-US",
  space: "preserve",
  id: "article1",
  base: "http://example.com/",
  title: "Title",
  content: "  Content with spaces  "
)

puts article.to_xml

Output:

<article xml:lang="en-US" xml:space="preserve" xml:id="article1" xml:base="http://example.com/">
  <title>Title</title>
  Content with spaces
</article>

Invalid usage raises errors:

# Invalid xml:space value
Article.new(space: "compact")
# => ArgumentError: xml:space must be 'default' or 'preserve'

# All W3C xml attributes serialize with xml: prefix
# No xmlns:xml declaration is ever generated

This enables automatic namespace qualification of elements and attributes based on their type, following W3C XML Namespace specifications.

This approach centralizes namespace configuration and enables:

  • Reusable namespace definitions across models

  • Full XSD generation support with proper namespace declarations

  • Control over element and attribute qualification

  • Documentation and versioning of schemas

  • Schema imports and includes

According to the W3C XML Namespace specification, unprefixed elements and attributes are handled differently:

  • unprefixed elements are considered to be in the default namespace (the declared default namespace of the parent, or the blank namespace if none is declared)

  • unprefixed attributes are considered to be in the blank namespace unless explicitly declared with a prefix.

Lutaml Models and Value types can each be specified to belong to an XML namespace via their respective namespace methods (model: namespace method in the xml block, value: xml_namespace method in the class). These models and types will always serialize their elements and attributes qualified with their namespace, either as the default namespace or with a prefix.

For models (i.e. serialized into XSD Types) and value types, the element_form_default and attribute_form_default settings in the XmlNamespace class are used to control the qualification of them within the parent.

For example:

Example 11. Unqualified elements and attributes by default
class MyNamespace < Lutaml::Model::XmlNamespace
  uri 'http://example.com/myns'
  prefix_default 'myns'
  element_form_default :qualified
  attribute_form_default :unqualified
end

class MyModel < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :age, :integer

  xml do
    element 'MyType'
    namespace MyNamespace
    map_element 'name', to: :name
    map_attribute 'age', to: :age
  end
end

my_model = MyModel.new(name: "Example", age: 30)

puts my_model.to_xml # defaults to using default prefix
#=>
  <MyType xmlns:hello="http://example.com/myns" age="30">
    <name>Example</name>
  </MyType>
# Note: No prefix on elements (W3C Namespace standard says unprefixed elements
# are in default namespace), no prefix on attribute (W3C Namespace standard says
# unprefixed attributes are in blank namespace)

puts my_model.to_xml(prefix: "hello") # Force custom prefix usage
#=>
  <hello:MyType xmlns:hello="http://example.com/myns" age="30">
    <hello:name>Example</hello:name>
  </hello:MyType>
# Note: Prefix on elements (qualified), no prefix on attribute (unqualified)
Example 12. Qualified attributes force prefixes
class MyNamespace < Lutaml::Model::XmlNamespace
  uri 'http://example.com/myns'
  prefix_default 'myns'
  element_form_default :qualified
  attribute_form_default :qualified
end

class MyModel < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :age, :integer

  xml do
    element 'MyType'
    namespace MyNamespace
    map_element 'name', to: :name
    map_attribute 'age', to: :age
  end
end

my_model = MyModel.new(name: "Example", age: 30)
puts my_model.to_xml # defaults to using default prefix, prefix is forced
# because attribute_form_default is :qualified, an attribute MUST be prefixed
# to indicate its namespace, hence the parent element must provide a prefixed
# namespace.
#=>
  <myns:MyType xmlns:myns="http://example.com/myns" myns:age="30">
    <myns:name>Example</myns:name>
  </myns:MyType>

Remember it is only the no-namespace model attributes that are affected by these settings of element_form_default and attribute_form_default. If an attribute or element is mapped to a type that belongs to a namespace, it will be qualified according to that namespace’s rules.

Example 13. Different element and attribute namespaces
class ElementNamespace < Lutaml::Model::XmlNamespace
  uri 'http://example.com/elementns'
  prefix_default 'elns'
end

class AttributeNamespace < Lutaml::Model::XmlNamespace
  uri 'http://example.com/attributens'
  prefix_default 'atns'
end

class AttributeValue < Lutaml::Model::Type::String
  xml_namespace AttributeNamespace
end

class MyModel < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :type, AttributeValue

  xml do
    element 'MyType'
    namespace ElementNamespace
    map_element 'name', to: :name
    map_attribute 'type', to: :type
  end
end

my_model = MyModel.new(name: "Example", type: "Special")
puts my_model.to_xml # element uses element namespace (which defaults to the
# default namespace), attribute uses attribute namespace, forcing a prefixed
# namespace
#=>
  <MyType xmlns="http://example.com/elementns"
          xmlns:atns="http://example.com/attributens"
          atns:type="Special">
    <name>Example</name>
  </MyType>

Creating namespace classes

A namespace class inherits from Lutaml::Model::XmlNamespace and uses a DSL to declare metadata.

Syntax:

class MyNamespace < Lutaml::Model::XmlNamespace
  uri 'namespace-uri'                    # Required: namespace URI
  schema_location 'schema-url'           # Optional: XSD location
  prefix_default 'default-prefix'        # Optional: default prefix
  element_form_default :qualified        # Optional: element qualification
  attribute_form_default :unqualified    # Optional: attribute qualification
  version 'version-string'               # Optional: schema version
  documentation 'description'            # Optional: schema documentation
  imports OtherNamespace                 # Optional: imported namespaces
  includes 'schema-file.xsd'             # Optional: included schemas
end

DSL methods

uri

Sets the namespace URI that uniquely identifies this namespace.

Syntax:

uri 'namespace-uri-string'

This is the fundamental identifier for the namespace, used in xmlns declarations.

Example 14. Setting namespace URI
class CeramicNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/schemas/ceramic/v1'
end

# Results in XML: xmlns="https://example.com/schemas/ceramic/v1"
# Or with prefix: xmlns:cer="https://example.com/schemas/ceramic/v1"
schema_location

Sets the URL where the XSD schema file can be found.

Syntax:

schema_location 'schema-url-or-path'

Used in xsi:schemaLocation attributes for schema validation.

Example 15. Setting schema location
class CeramicNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/schemas/ceramic/v1'
  schema_location 'https://example.com/schemas/ceramic/v1/ceramic.xsd'
end
prefix_default

Sets the default prefix for this namespace.

Syntax:

prefix_default 'prefix' # or :prefix (symbol)

The prefix can be overridden at runtime when creating namespace instances.

Example 16. Setting default prefix
class CeramicNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/schemas/ceramic/v1'
  prefix_default 'cer'
end

# Default usage: xmlns:cer="https://..."
# Runtime override: xmlns:ceramic="https://..."
element_form_default

Controls whether locally declared elements must be namespace-qualified by default.

Syntax:

element_form_default :qualified   # or :unqualified (default)

Values:

:qualified

Local elements must include namespace prefix

:unqualified

Local elements have no namespace prefix (default)

Example 17. Setting element qualification default
class CeramicNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/schemas/ceramic/v1'
  prefix_default 'cer'
  element_form_default :qualified
end

# With :qualified, child elements get: <cer:type>...</cer:type>
# With :unqualified, child elements get: <type>...</type>
attribute_form_default

Controls whether locally declared attributes must be namespace-qualified by default.

Syntax:

attribute_form_default :qualified   # or :unqualified (default)

Values:

:qualified

Local attributes must include namespace prefix

:unqualified

Local attributes have no namespace prefix (default)

Per W3C conventions, attributes are typically unqualified.
Example 18. Setting attribute qualification default
class CeramicNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/schemas/ceramic/v1'
  prefix_default 'cer'
  attribute_form_default :unqualified  # Typically left unqualified
end
imports

Declares dependencies on other namespaces via XSD import directive.

Syntax:

imports OtherNamespace1, OtherNamespace2, ...

Used when referencing types from other namespaces.

Example 19. Importing other namespaces
class AddressNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/schemas/address/v1'
  prefix_default 'addr'
end

class ContactNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/schemas/contact/v1'
  prefix_default 'contact'
  imports AddressNamespace  # Import address namespace
end

# Generates in XSD:
# <xs:import namespace="https://example.com/schemas/address/v1"
#            schemaLocation="..." />
includes

Declares schema components from the same namespace via XSD include directive.

Syntax:

includes 'schema-file.xsd', 'another-file.xsd', ...

Used for modular schema organization within the same namespace.

Example 20. Including schema files
class ContactNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/schemas/contact/v1'
  prefix_default 'contact'
  includes 'contact-common.xsd', 'contact-extensions.xsd'
end

# Generates in XSD:
# <xs:include schemaLocation="contact-common.xsd" />
# <xs:include schemaLocation="contact-extensions.xsd" />
version

Sets the schema version for documentation and tracking.

Syntax:

version 'version-string'
Example 21. Setting schema version
class ContactNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/schemas/contact/v1'
  version '1.0.0'
end

# Used in XSD: <xs:schema version="1.0.0">
documentation

Provides human-readable description for XSD annotation.

Syntax:

documentation 'description text'
Example 22. Adding documentation
class ContactNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/schemas/contact/v1'
  documentation "Contact information schema for Example Corp"
end

# Generates in XSD:
# <xs:annotation>
#   <xs:documentation>Contact information schema for Example Corp</xs:documentation>
# </xs:annotation>

Complete namespace example

Example 23. Fully configured XML namespace
# Define dependent namespace
class AddressNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/schemas/address/v1'
  schema_location 'https://example.com/schemas/address/v1/address.xsd'
  prefix_default 'addr'
end

# Define main namespace with all features
class ContactNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/schemas/contact/v1'
  schema_location 'https://example.com/schemas/contact/v1/contact.xsd'
  prefix_default 'contact'
  element_form_default :qualified
  attribute_form_default :unqualified
  version '1.0'
  documentation "Contact information schema for Example Corp"

  imports AddressNamespace
  includes 'contact-common.xsd', 'contact-types.xsd'
end

# Use namespace in model
class Person < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :email, :string

  xml do
    element "person"
    namespace ContactNamespace

    sequence do
      map_element "name", to: :name
      map_element "email", to: :email
    end
  end
end

person = Person.new(name: "John Doe", email: "john@example.com")
puts person.to_xml
# => <contact:person xmlns:contact="https://example.com/schemas/contact/v1">
#      <contact:name>John Doe</contact:name>
#      <contact:email>john@example.com</contact:email>
#    </contact:person>

Namespace identity of element or type (namespace method)

Declaration of namespace

The namespace method in the xml block indicates that the element or type belongs to the specified XML namespace.

Syntax:

Setting namespace of the XML element or XML type using a XmlNamespace
# Assume we have defined an XmlNamespace class called ExampleXmlNamespace

xml do
  namespace ExampleXmlNamespace
end
Example 24. Using the namespace method to set namespace for an element
class CeramicXmlNamespace
  uri 'http://example.com/ceramic'
  prefix_default 'cer'
end

class Ceramic < Lutaml::Model::Serializable
  attribute :type, :string
  attribute :glaze, :string

  xml do
    element 'Ceramic'
    namespace CeramicXmlNamespace
    map_element 'Type', to: :type
    map_element 'Glaze', to: :glaze
  end
end
<Ceramic xmlns='http://example.com/ceramic'>
  <Type>Porcelain</Type>
  <Glaze>Clear</Glaze>
</Ceramic>

By default, serialization using to_xml uses the namespace as the XML default namespace.

> Ceramic.from_xml(xml_file)
> #<Ceramic:0x0000000104ac7240 @type="Porcelain", @glaze="Clear">
> Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml
> #<Ceramic xmlns="http://example.com/ceramic">
  #  <Type>Porcelain</Type>
  #  <Glaze>Clear</Glaze>
  #</Ceramic>

Use the #to_xml prefix: true option to force the defined prefix:

> Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml(prefix: true)
> #<cer:Ceramic xmlns:cer="http://example.com/ceramic">
  #  <cer:Type>Porcelain</cer:Type>
  #  <cer:Glaze>Clear</cer:Glaze>
  #</cer:Ceramic>

Use the #to_xml prefix: "custom" option to forces a custom prefix:

> Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml(prefix: "custom")
> #<custom:Ceramic xmlns:custom="http://example.com/ceramic">
  #  <custom:Type>Porcelain</custom:Type>
  #  <custom:Glaze>Clear</custom:Glaze>
  #</custom:Ceramic>
Force display of unused prefixes

In some cases, you may want to declare additional namespaces in the XML output even if they are not used by any elements.

This is technically not needed by W3C standards conformant XML processors, but some legacy XML processors may require it.

In this case, the namespace_scope method can be used to force declaration of additional namespaces to be included in the output.

Example 25. Default namespace with additional prefixed namespaces
class AppNamespace < Lutaml::Model::XmlNamespace
  uri 'http://schemas.openxmlformats.org/officeDocument/2006/extended-properties'
  prefix_default 'app'
end

class VtNamespace < Lutaml::Model::XmlNamespace
  uri 'http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes'
  prefix_default 'vt'
end

class Properties < Lutaml::Model::Serializable
  attribute :template, :string

  xml do
    element "Properties"
    namespace AppNamespace

    # Force vt namespace to be declared even if unused
    namespace_scope [
      { namespace: VtNamespace, declare: :always }
    ]

    map_element "Template", to: :template
  end
end

props = Properties.new(template: "Normal.dotm")
puts props.to_xml  # Default: root namespace is default, others prefixed

Output:

<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
            xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
  <Template>Normal.dotm</Template>
</Properties>

With prefix: true option:

puts props.to_xml(prefix: true)

Output:

<app:Properties xmlns:app="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
                xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
  <app:Template>Normal.dotm</app:Template>
</app:Properties>
The VtNamespace always uses "vt:" prefix regardless of the prefix: option.
Namespace prefix override

Namespace prefixes can be overridden when building namespace instances.

Example 26. Overriding namespace prefix
class ContactNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/schemas/contact/v1'
  prefix_default 'contact'
end

# Use default prefix
class Person < Lutaml::Model::Serializable
  xml do
    element "person"
    namespace ContactNamespace  # Uses 'contact' prefix
  end
end

# Override prefix at mapping level
class ShortPerson < Lutaml::Model::Serializable
  xml do
    element "person"
    namespace ContactNamespace, 'c'  # Override to 'c' prefix
  end
end

Person.new.to_xml
# => <contact:person xmlns:contact="...">...</contact:person>

ShortPerson.new.to_xml
# => <c:person xmlns:c="...">...</c:person>

Namespace scope consolidation (namespace_scope method)

For detailed information on xmlns declaration strategies and the minimal-subtree principle, see XML Namespace Declarations Guide.
General

The namespace_scope directive controls where namespace declarations are eligible to appear in serialized XML.

By default, namespaces are declared on the elements where they are used. With namespace_scope, you can consolidate multiple namespace declarations at a parent element for cleaner, more compact XML.

The namespace_scope directive supports a declare: option to force unused namespaces to be included in the output.

Syntax:

xml do
  namespace RootNamespace
  namespace_scope [Namespace1, Namespace2, Namespace3]  (1)
  # is identical to:
  # namespace_scope [
  #   { namespace: Namespace1, declare: :auto },
  #   { namespace: Namespace2, declare: :auto },
  #   { namespace: Namespace3, declare: :auto }
  # ]

  # Per-namespace control with hash format
  namespace_scope [  (2)
    { namespace: Namespace1, declare: :always },
    { namespace: Namespace2, declare: :auto },
    # Namespace3  # CANNOT mix hash and class
  ]
end
1 Simple list of XmlNamespace classes to declare at root (default: :auto)
2 Per-namespace control with individual declare: settings using hash format

Where,

namespace_scope

Array of XmlNamespace class objects or Hash configurations. These namespaces will be declared at the root element based on their declaration mode.

declare:

Controls when namespace is declared:

:auto

(default) Declare only if namespace is actually used in child elements/attributes (or the element itself)

:always

Always declare namespace, even if unused in child elements/attributes or the element itself

:never

Never declare (error if used)

Basic namespace consolidation
Example 27. Namespace consolidation with vCard (using default :auto mode)
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

# Types with different namespaces
class DcTitleType < Lutaml::Model::Type::String
  xml_namespace DcNamespace
end

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

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

  xml do
    element "vCard"
    namespace VcardNamespace
    namespace_scope [VcardNamespace, DcNamespace, DctermsNamespace]  (1)

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

vcard = Vcard.new(
  title: "Dr. John Doe",
  created: DateTime.parse("2024-06-01T12:00:00Z")
)

puts vcard.to_xml
1 Consolidate all three namespaces at root element (default :auto mode)

Output with default namespace (no prefix option):

<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/">
  <dc:title>Dr. John Doe</dc:title>
  <dcterms:created>2024-06-01T12:00:00+00:00</dcterms:created>
</vCard>

Without namespace_scope, each namespace would be declared locally:

<vCard xmlns="urn:ietf:params:xml:ns:vcard-4.0">
  <dc:title xmlns:dc="http://purl.org/dc/elements/1.1/">Dr. John Doe</dc:title>
  <dcterms:created xmlns:dcterms="http://purl.org/dc/terms/">2024-06-01T12:00:00+00:00</dcterms:created>
</vCard>
Forcing unused namespace declarations

Use declare: :always to force namespace declarations even when not used:

Example 28. Always declare namespace with declare: :always
class AppNamespace < Lutaml::Model::XmlNamespace
  uri 'http://schemas.openxmlformats.org/officeDocument/2006/extended-properties'
  prefix_default 'app'
end

class VtNamespace < Lutaml::Model::XmlNamespace
  uri 'http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes'
  prefix_default 'vt'
end

class Properties < Lutaml::Model::Serializable
  attribute :template, :string

  xml do
    element "Properties"
    namespace AppNamespace

    # Force vt namespace declaration even though unused
    namespace_scope [
      { namespace: VtNamespace, declare: :always }
    ]

    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"
            xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
  <Template>Normal.dotm</Template>
</Properties>
The vt: namespace is declared even though no elements use it. This is required by some XML consumers like Office Open XML.
Per-namespace declaration control

Use Hash format for fine-grained control over individual namespaces:

Example 29. Mixed declaration modes with per-namespace control
class CorePropertiesNamespace < 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 XsiNamespace < Lutaml::Model::XmlNamespace
  uri 'http://www.w3.org/2001/XMLSchema-instance'
  prefix_default 'xsi'
end

class CoreProperties < Lutaml::Model::Serializable
  attribute :title, :string

  xml do
    element "coreProperties"
    namespace CorePropertiesNamespace

    # Per-namespace declaration control
    namespace_scope [
      { namespace: DcNamespace, declare: :auto },     (1)
      { namespace: XsiNamespace, declare: :always }   (2)
    ]

    map_element "title", to: :title
  end
end
1 DcNamespace declared only if used (:auto mode)
2 XsiNamespace always declared even if unused (:always mode)

When DcNamespace is used:

<coreProperties xmlns="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
                xmlns:dc="http://purl.org/dc/elements/1.1/"
                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <dc:title>Document Title</dc:title>
</coreProperties>

When DcNamespace is not used (title remains unset or uses different namespace):

<coreProperties xmlns="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
</coreProperties>
XsiNamespace is always declared (:always), while DcNamespace is omitted when unused (:auto).
Declaration modes
Mode Behavior

:auto (default)

Namespace declared only if actually used in elements or attributes

:always

Namespace always declared at root, even if unused. Use for schema compliance or when external tools require namespace presence.

:never

Namespace never declared. Error raised if namespace is used in elements. Reserved for future use.

Use cases for declaration modes

Use :auto mode (default) when:

  • Standard W3C namespace behavior desired

  • Minimize unnecessary namespace declarations

  • Generate clean, minimal XML

  • Examples: Most XML documents, APIs

Use :always mode when:

  • Schema requires specific namespace declarations

  • External tools validate namespace presence

  • Format specifications mandate unused namespaces

  • Examples: Office Open XML (xmlns:vt required), SOAP envelopes

Use :never mode when:

  • Explicitly prevent namespace usage

  • Catch errors during development

  • Reserved for future extensibility

Form override (W3C Schema compliance)

Per W3C XML Schema specification, the form attribute controls whether elements must be qualified (prefixed) or unqualified (no prefix) on a per-element basis.

When to use form override

Use form when:

  • Schema specifies elementFormDefault="qualified" but some elements should be local

  • Schema specifies elementFormDefault="unqualified" but some elements need qualification

  • Mixing qualified and unqualified elements in same parent

Syntax
xml do
  map_element 'name', to: :attr, form: :qualified    # Force prefix
  map_element 'name', to: :attr, form: :unqualified  # Force no prefix
end
Example: Local elements in qualified schema
Example 30. Overriding element qualification with form
class ReportNamespace < Lutaml::Model::XmlNamespace
  uri "http://example.com/reports"
  prefix_default "r"
  element_form_default :qualified  # All elements qualified by default
end

class Report < Lutaml::Model::Serializable
  attribute :title, :string
  attribute :metadata, :string

  xml do
    element "report"
    namespace ReportNamespace

    map_element "title", to: :title                          # Qualified (follows default)
    map_element "metadata", to: :metadata, form: :unqualified  # Local override
  end
end

report = Report.new(title: "Annual Report", metadata: "Internal use")
puts report.to_xml(prefix: true)

Output:

<r:report xmlns:r="http://example.com/reports">
  <r:title>Annual Report</r:title>
  <metadata>Internal use</metadata>  <!-- No prefix, local scope -->
</r:report>
The metadata element is unqualified despite element_form_default: :qualified. The form: :unqualified override forces local scope.
Priority order

Form takes highest priority in qualification decisions:

  1. form: :unqualified → Forces NO prefix (local scope)

  2. form: :qualified → Forces prefix usage

  3. namespace: :inherit → Uses parent namespace

  4. Type namespace → Uses type’s namespace

  5. Schema elementFormDefault → Follows default

  6. Unqualified → No namespace

XSD annotation (documentation method)

The documentation method adds human-readable description for XSD generation.

Syntax:

xml do
  element 'element-name'
  documentation 'Description text for XSD annotation'
end

Used in generated XSD <xs:annotation>…​<xs:documentation> elements.

Example 31. Adding documentation to model
class Product < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :price, :float

  xml do
    element 'product'
    documentation "Represents a product in the catalog"

    map_element 'name', to: :name
    map_element 'price', to: :price
  end
end

# When generating XSD from this model:
# <xs:complexType name="ProductType">
#   <xs:annotation>
#     <xs:documentation>Represents a product in the catalog</xs:documentation>
#   </xs:annotation>
#   ...
# </xs:complexType>

XSD type name declaration (type_name method)

The type_name method sets an explicit type name for XSD generation.

By default, type names are inferred as {ClassName}Type. This method is only to override that value.

This method is only useful for customizing a generated XSD schema file from the models, the XSD type name does not affect XML processing or the functioning of the resulting XSD schema.

Customizing the type name is typically used to make the XSD compatible with external schemas that define or reuse the same XSD type.

Syntax:

xml do
  element 'element-name'
  type_name 'CustomTypeName'
end
Example 32. Setting custom type name
class Product < Lutaml::Model::Serializable
  attribute :name, :string

  xml do
    element 'product'
    type_name 'CatalogItemType'  # Override default 'ProductType'

    map_element 'name', to: :name
  end
end

# Generated XSD uses: <xs:complexType name="CatalogItemType">

XSD sequence requirements (sequence block)

Lutaml::Model supports the XSD notions of declaring structural requirements on XML elements and types.

Usage of these declarations allows for precise control over the structure and validation of XML documents. These notions directly translate to the XSD equivalents and is reflected in the corresponding XSD generated by Lutaml::Model as XML complex types.

The sequence directive specifies that the defined attributes must appear in a specified order in XML.

Sequence only supports map_element mappings.

Syntax:

xml do
  sequence do
    map_element 'xml_element_name_1', to: :name_of_attribute_1
    map_element 'xml_element_name_2', to: :name_of_attribute_2
    # Add more map_element lines as needed to establish a complete sequence
  end
end

The appearance of the elements in the XML document must match the order defined in the sequence block. In this case, the <xml_element_name_1> element should appear before the <xml_element_name_2> element.

Example 33. Using the sequence keyword to define a set of elements in desired order.
class Kiln < Lutaml::Model::Serializable
  attribute :id, :string
  attribute :name, :string
  attribute :type, :string
  attribute :color, :string

  xml do
    sequence do
      map_element :id, to: :id
      map_element :name, to: :name
      map_element :type, to: :type
      map_element :color, to: :color
    end
  end
end

class KilnCollection < Lutaml::Model::Serializable
  attribute :kiln, Kiln, collection: 1..2

  xml do
    element "collection"
    map_element "kiln", to: :kiln
  end
end
<collection>
  <kiln>
    <id>1</id>
    <name>Nick</name>
    <type>Hard</type>
    <color>Black</color>
  </kiln>
  <kiln>
    <id>2</id>
    <name>John</name>
    <type>Soft</type>
    <color>White</color>
> parsed = Kiln.from_xml(xml)
# => [
#<Kiln:0x0000000104ac7240 @id="1", @name="Nick", @type="Hard", @color="Black">,
#<Kiln:0x0000000104ac7240 @id="2", @name="John", @type="Soft", @color="White">
#]

> bad_xml = <<~HERE
<collection>
  <kiln>
    <name>Nick</name>
    <id>1</id>
    <color>Black</color>
    <type>Hard</type>
  </kiln>
</collection>
HERE
> parsed = Kiln.from_xml(bad_xml)
# => Lutaml::Model::ValidationError: Element 'name' is out of order in 'kiln' element
For importing model mappings inside a sequence block, refer to Importing model mappings inside a sequence.

Automatic support of xsi:schemaLocation

The W3C "XMLSchema-instance" namespace describes a number of attributes that can be used to control the behavior of XML processors. One of these attributes is xsi:schemaLocation.

The xsi:schemaLocation attribute locates schemas for elements and attributes that are in a specified namespace. Its value consists of pairs of a namespace URI followed by a relative or absolute URL where the schema for that namespace can be found.

Usage of xsi:schemaLocation in an XML element depends on the declaration of the XML namespace of xsi, i.e. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance". Without this namespace LutaML will not be able to serialize the xsi:schemaLocation attribute.

It is most commonly attached to the root element but can appear further down the tree.

The following snippet shows how xsi:schemaLocation is used in an XML document:

<cera:Ceramicın
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:cera="http://example.com/ceramic"
  xmlns:clr='http://example.com/color'
  xsi:schemaLocation=
    "http://example.com/ceramic http://example.com/ceramic.xsd
     http://example.com/color http://example.com/color.xsd"
  clr:color="navy-blue">
  <cera:Type>Porcelain</cera:Type>
  <Glaze>Clear</Glaze>
</cera:Ceramicın>

LutaML::Model supports the xsi:schemaLocation attribute in all XML serializations by default, through the schema_location attribute on the model instance object.

Example 34. Retrieving and setting the xsi:schemaLocation attribute in XML serialization

In this example, the xsi:schemaLocation attribute will be automatically supplied without the explicit need to define in the model, and allows for round-trip serialization.

class ColorXmlNamespace < Lutaml::Model::XmlNamespace
  uri 'http://example.com/color'
  default_prefix 'clr'
end

class Ceramic < Lutaml::Model::Serializable
  attribute :type, :string
  attribute :glaze, :string
  attribute :color, :string

  xml do
    element 'Ceramic'
    namespace 'http://example.com/ceramic', 'cera'
    map_element 'Type', to: :type  # Inherits parent namespace
    map_element 'Glaze', to: :glaze
    map_attribute 'color', to: :color  # Per W3C, attributes don't inherit namespace
  end
end

xml_content = <<~HERE
<cera:Ceramic
  xmlns:cera="http://example.com/ceramic"
  xmlns:clr="http://example.com/color"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  clr:color="navy-blue"
  xsi:schemaLocation="
    http://example.com/ceramic http://example.com/ceramic.xsd
    http://example.com/color http://example.com/color.xsd
  ">
  <cera:Type>Porcelain</cera:Type>
  <Glaze>Clear</Glaze>
</cera:Ceramic>
HERE
> c = Ceramic.from_xml(xml_content)
=>
#<Ceramic:0x00000001222bdd60
...
> schema_loc = c.schema_location
#<Lutaml::Model::SchemaLocation:0x0000000122773760
...
> schema_loc
=>
#<Lutaml::Model::SchemaLocation:0x0000000122773760
 @namespace="http://www.w3.org/2001/XMLSchema-instance",
 @original_schema_location="http://example.com/ceramic http://example.com/ceramic.xsd http://example.com/color http://example.com/color.xsd",
 @prefix="xsi",
 @schema_location=
  [#<Lutaml::Model::Location:0x00000001222bd018 @location="http://example.com/ceramic.xsd", @namespace="http://example.com/ceramic">,
   #<Lutaml::Model::Location:0x00000001222bcfc8 @location="http://example.com/color.xsd", @namespace="http://example.com/color">]>
> new_c = Ceramic.new(type: "Porcelain", glaze: "Clear", color: "navy-blue", schema_location: schema_loc).to_xml
> puts new_c
# <cera:Ceramic
#   xmlns:cera="http://example.com/ceramic"
#   xmlns:clr="http://example.com/color"
#   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
#   clr:color="navy-blue"
#   xsi:schemaLocation="
#     http://example.com/ceramic http://example.com/ceramic.xsd
#     http://example.com/color http://example.com/color.xsd
#   ">
#   <cera:Type>Porcelain</cera:Type>
#   <cera:Glaze>Clear</cera:Glaze>
# </cera:Ceramic>
For details on xsi:schemaLocation, please refer to the W3C XML standard.

Deprecated ways of working with namespaces

Custom namespace prefixes

LutaML Model allows you to customize the namespace prefix used during XML serialization through the prefix option on to_xml.

Usage

The prefix option accepts three types of values:

String

Use a custom prefix for serialization

Boolean true

Use the namespace’s configured prefix_default

Boolean false

Use default namespace format (xmlns="…​", no prefix)

class Document < Lutaml::Model::Serializable
  attribute :title, :string

  xml do
    namespace DocumentNamespace  # has prefix_default "doc"
    element "Document"
    map_element "title", to: :title
  end
end

doc = Document.new(title: "Example")

# Default format
doc.to_xml
# => <Document xmlns="http://example.com">...</Document>

# Use namespace's default prefix
doc.to_xml(prefix: true)
# => <doc:Document xmlns:doc="http://example.com">...</doc:Document>

# Use custom prefix
doc.to_xml(prefix: "d")
# => <d:Document xmlns:d="http://example.com">...</d:Document>

Prefix inheritance

Custom prefixes are automatically inherited by child elements in the same namespace:

parent.to_xml(prefix: "custom")
# All elements in same namespace use "custom:" prefix

Namespace identity vs presentation

The prefix option controls presentation (default vs prefix format), not namespace identity (which URI). The same namespace URI with different prefixes is still the same namespace semantically.
(DEPRECATED) Setting default namespace at the root element
xml do
  namespace 'http://example.com/namespace'
end
(DEPRECATED) Setting a prefixed namespace at the root element
xml do
  namespace 'http://example.com/namespace', 'prefix'
end

Table of contents