Overview

This guide helps you migrate from the old XML mapping approach to the new namespace-aware architecture introduced in Lutaml::Model.

Key changes

From root() to element()

Old approach:

class Ceramic < Lutaml::Model::Serializable
  xml do
    root 'ceramic'
    map_element 'type', to: :type
  end
end

New approach (recommended):

class Ceramic < Lutaml::Model::Serializable
  xml do
    element 'ceramic'  # Modern API
    map_element 'type', to: :type
  end
end
root() still works as a backward-compatible alias. No breaking changes.

From no_root to type-only models

Old approach (deprecated):

class Address < Lutaml::Model::Serializable
  xml do
    no_root  # DEPRECATED - shows warning
    map_element 'street', to: :street
    map_element 'city', to: :city
  end
end

New approach (recommended):

class Address < Lutaml::Model::Serializable
  xml do
    # No element() or root() call - this is a type-only model
    sequence do
      map_element 'street', to: :street
      map_element 'city', to: :city
    end
  end
end

Important: Type-only models can only be parsed when embedded in parent models, not standalone.

From string namespaces to XmlNamespace classes

Old approach (still works):

class Ceramic < Lutaml::Model::Serializable
  xml do
    root 'ceramic'
    namespace 'http://example.com/ceramic', 'cer'
    map_element 'type', to: :type
  end
end

New approach (recommended):

class CeramicNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/ceramic'
  prefix_default 'cer'
  element_form_default :qualified
  version '1.0'
  documentation "Ceramic schema"
end

class Ceramic < Lutaml::Model::Serializable
  xml do
    element 'ceramic'
    namespace CeramicNamespace
    map_element 'type', to: :type
  end
end

Benefits: * Centralized namespace metadata * Reusable across models * Full XSD generation support * Qualification control * Version and documentation tracking

Migration steps

Step 1: Identify models to migrate

Find models using: * no_root - Needs migration to type-only pattern * String-based namespace() - Consider XmlNamespace classes for better organization

Step 2: Update type-only models

For each model with no_root:

  1. Remove the no_root call

  2. Ensure no element() or root() call exists

  3. Keep all mappings (map_element, etc.)

  4. Consider wrapping in sequence if order matters

Step 3: Create XmlNamespace classes (optional)

For models with complex namespace requirements:

  1. Create a namespace class

  2. Define all metadata (URI, prefix, qualification, etc.)

  3. Replace string-based namespace() with class reference

Step 4: Update element declarations (optional)

Replace root() with element() for clarity:

  1. Change root 'element-name' to element 'element-name'

  2. Move mixed: and ordered: options if needed

  3. Or use mixed_content() method explicitly

Step 5: Test thoroughly

  • Verify round-trip serialization/deserialization

  • Check namespace prefixes in output

  • Validate against XSD if applicable

  • Run existing tests to ensure no regressions

Migration examples

Example 1: Simple no_root migration

Before:

class Address < Lutaml::Model::Serializable
  attribute :street, :string
  attribute :city, :string

  xml do
    no_root
    map_element 'street', to: :street
    map_element 'city', to: :city
  end
end

After:

class Address < Lutaml::Model::Serializable
  attribute :street, :string
  attribute :city, :string

  xml do
    # No element declaration needed
    sequence do
      map_element 'street', to: :street
      map_element 'city', to: :city
    end
  end
end

Example 2: Namespace class migration

Before:

class Contact < Lutaml::Model::Serializable
  attribute :name, :string

  xml do
    root 'contact'
    namespace 'https://example.com/contact/v1', 'contact'
    map_element 'name', to: :name
  end
end

class Person < Lutaml::Model::Serializable
  attribute :full_name, :string

  xml do
    root 'person'
    namespace 'https://example.com/contact/v1', 'contact'
    map_element 'fullName', to: :full_name
  end
end

After:

# Define once, reuse everywhere
class ContactNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/contact/v1'
  prefix_default 'contact'
  element_form_default :qualified
  version '1.0'
end

class Contact < Lutaml::Model::Serializable
  attribute :name, :string

  xml do
    element 'contact'
    namespace ContactNamespace  # Reuse
    map_element 'name', to: :name
  end
end

class Person < Lutaml::Model::Serializable
  attribute :full_name, :string

  xml do
    element 'person'
    namespace ContactNamespace  # Reuse
    map_element 'fullName', to: :full_name
  end
end

Example 3: Complete migration

Before:

class Metadata < Lutaml::Model::Serializable
  attribute :version, :string

  xml do
    no_root
    map_attribute 'version', to: :version
  end
end

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

  xml do
    root 'document'
    namespace 'https://example.com/doc', 'doc'
    map_element 'title', to: :title
    map_element 'metadata', to: :metadata
  end
end

After:

class DocNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/doc'
  prefix_default 'doc'
  element_form_default :qualified
  version '2.0'
  documentation "Document schema v2"
end

# Type-only model
class Metadata < Lutaml::Model::Serializable
  attribute :version, :string

  xml do
    # No element() - type-only
    map_attribute 'version', to: :version
  end
end

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

  xml do
    element 'document'
    namespace DocNamespace
    documentation "A document with metadata"

    sequence do
      map_element 'title', to: :title
      map_element 'metadata', to: :metadata
    end
  end
end

Compatibility notes

What still works

  • root() - Maintained as alias to element()

  • String-based namespace(uri, prefix) - Still fully supported

  • no_root - Works but shows deprecation warning

  • All existing XML mappings - No breaking changes

What’s new (opt-in)

  • element() - Modern API for element declaration

  • XmlNamespace classes - Centralized namespace metadata

  • Type-only models via absence of element declaration

  • form: option - Per-element/attribute qualification control

  • documentation: option - XSD annotation support

  • Enhanced XSD generation with full namespace support

Deprecation timeline

  • no_root - Deprecated, will be removed in future major version

  • Migration path: Simply omit element() or root() call

  • No other deprecations currently planned

Incremental migration strategy

You don’t need to migrate everything at once. Consider this approach:

  1. Phase 1: Update no_root models to type-only pattern

    • Low risk, high clarity improvement

    • Can be done model-by-model

  2. Phase 2: Replace root() with element() in new code

    • No urgency, just style preference

    • Makes intent clearer

  3. Phase 3: Introduce XmlNamespace classes where beneficial

    • For projects with multiple models in same namespace

    • When generating XSD

    • When documentation is important

  4. Phase 4: Add metadata as needed

    • documentation for XSD generation

    • type_name for custom type names

    • form: for qualification control

Testing after migration

Ensure your migrated code works correctly:

# Round-trip test
original = YourModel.new(attr: "value")
xml = original.to_xml
parsed = YourModel.from_xml(xml)
assert_equal original, parsed

# Namespace test
assert_includes xml, 'xmlns:prefix="namespace-uri"'

# XSD generation test (if using)
xsd = Lutaml::Model::Schema.to_xml(YourModel)
assert_includes xsd, '<xs:schema'
assert_includes xsd, 'targetNamespace'

Deprecated syntax migration

Type::Value namespace directive

What changed

The namespace directive for Type::Value classes has been renamed to xml_namespace.

Why the change

  • XML-specific clarity: Clearly indicates this is XML serialization configuration

  • Future compatibility: Reserves namespace for semantic namespace features (JSON-LD @context, RDF IRIs)

  • Consistency: Maintains consistency with format-specific configuration patterns

  • Disambiguation: Distinguishes Type-level namespace from Model-level namespace directive

Migration steps

  1. Identify affected classes

    Find all Type::Value subclasses using namespace:

    # Search for patterns like:
    class CustomType < Lutaml::Model::Type::String
      namespace SomeNamespace  # ⚠️ DEPRECATED
    end
  2. Replace with xml_namespace

    class CustomType < Lutaml::Model::Type::String
      xml_namespace SomeNamespace  # ✅ CURRENT
    end
  3. No other changes needed

    Functionality remains identical, serialization and deserialization work the same

Backward compatibility

The deprecated namespace directive:

  • ✅ Continues to work in current versions

  • ⚠️ Shows deprecation warning: [DEPRECATION] Type::Value.namespace is deprecated. Use xml_namespace instead. This will be removed in version 1.0.0

  • ❌ Will be removed in Lutaml::Model version 1.0.0

Complete example

Before (deprecated)
class DublinCoreNamespace < Lutaml::Model::XmlNamespace
  uri 'http://purl.org/dc/elements/1.1/'
  prefix_default 'dc'
end

class DcTitleType < Lutaml::Model::Type::String
  namespace DublinCoreNamespace  # ⚠️ DEPRECATED
  xsd_type 'titleType'

  def self.cast(value)
    super(value)
  end
end

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

  xml do
    root 'document'
    map_element 'title', to: :title
  end
end
After (current)
class DublinCoreNamespace < Lutaml::Model::XmlNamespace
  uri 'http://purl.org/dc/elements/1.1/'
  prefix_default 'dc'
end

class DcTitleType < Lutaml::Model::Type::String
  xml_namespace DublinCoreNamespace  # ✅ CURRENT
  xsd_type 'titleType'

  def self.cast(value)
    super(value)
  end
end

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

  xml do
    root 'document'
    map_element 'title', to: :title
  end
end

Result: Both versions produce identical XML output and parse correctly, but the new syntax is future-proof and won’t trigger deprecation warnings.