Namespace practices

Use XmlNamespace classes for structured projects

Do:

# Define namespace once
class ProjectNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/project/v1'
  prefix_default 'proj'
  element_form_default :qualified
  version '1.0'
  documentation "Project schema"
end

# Reuse across many models
class Model1 < Lutaml::Model::Serializable
  xml do
    element 'model1'
    namespace ProjectNamespace
  end
end

class Model2 < Lutaml::Model::Serializable
  xml do
    element 'model2'
    namespace ProjectNamespace
  end
end

Don’t:

# Repeat namespace details in every model
class Model1 < Lutaml::Model::Serializable
  xml do
    root 'model1'
    namespace 'https://example.com/project/v1', 'proj'
  end
end

class Model2 < Lutaml::Model::Serializable
  xml do
    root 'model2'
    namespace 'https://example.com/project/v1', 'proj'
  end
end

Follow W3C qualification conventions

Do:

class MyNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/schema'
  prefix_default 'pfx'
  element_form_default :qualified    # Typical for complex schemas
  attribute_form_default :unqualified  # W3C convention
end

Don’t:

# Avoid qualifying attributes unless absolutely necessary
class MyNamespace < Lutaml::Model::XmlNamespace
  attribute_form_default :qualified  # Unusual, avoid unless required
end

Use meaningful namespace URIs

Do:

class ContactNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/schemas/contact/v1'  # Versioned, descriptive
  schema_location 'https://example.com/schemas/contact/v1/contact.xsd'
end

Don’t:

class ContactNamespace < Lutaml::Model::XmlNamespace
  uri 'http://example.com'  # Too generic
  uri 'urn:x-example:contact'  # Non-standard format without good reason
end

Use Type-level namespaces for reusable types

Do:

# Define namespace for Dublin Core
class DublinCoreNamespace < Lutaml::Model::XmlNamespace
  uri 'http://purl.org/dc/elements/1.1/'
  prefix_default 'dc'
end

# Define reusable DC types with namespace
class DcTitleType < Lutaml::Model::Type::String
  xml_namespace DublinCoreNamespace
  xsd_type 'titleType'
end

class DcCreatorType < Lutaml::Model::Type::String
  xml_namespace DublinCoreNamespace
  xsd_type 'creatorType'
end

# Use across multiple models - namespace applied automatically
class Document < Lutaml::Model::Serializable
  attribute :title, DcTitleType
  attribute :creator, DcCreatorType

  xml do
    root 'document'
    map_element 'title', to: :title      # dc:title automatically
    map_element 'creator', to: :creator  # dc:creator automatically
  end
end

class BookMetadata < Lutaml::Model::Serializable
  attribute :title, DcTitleType
  attribute :creator, DcCreatorType

  xml do
    root 'book'
    map_element 'title', to: :title      # dc:title automatically
    map_element 'creator', to: :creator  # dc:creator automatically
  end
end

Don’t:

# Repeat namespace on every element mapping
class Document < Lutaml::Model::Serializable
  attribute :title, :string
  attribute :creator, :string

  xml do
    root 'document'
    # Repetitive and error-prone
    map_element 'title', to: :title,
      namespace: 'http://purl.org/dc/elements/1.1/', prefix: 'dc'
    map_element 'creator', to: :creator,
      namespace: 'http://purl.org/dc/elements/1.1/', prefix: 'dc'
  end
end

Use namespace_scope for multi-namespace documents

Do:

# Consolidate frequently-used namespaces at root
class Vcard < Lutaml::Model::Serializable
  xml do
    root "vCard"
    namespace VcardNamespace
    namespace_scope [VcardNamespace, DcNamespace, DctermsNamespace]

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

# Result: Clean XML with all namespaces declared at root
# <vcard:vCard xmlns:vcard="..." xmlns:dc="..." xmlns:dcterms="...">
#   <vcard:version>4.0</vcard:version>
#   <dc:title>Contact: Dr. John Doe</dc:title>
#   <dcterms:created>2024-06-01T12:00:00Z</dcterms:created>
# </vcard:vCard>

Don’t:

# Without namespace_scope for multi-namespace documents
class Vcard < Lutaml::Model::Serializable
  xml do
    root "vCard"
    namespace VcardNamespace
    # No namespace_scope

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

# Result: Repetitive namespace declarations
# <vcard:vCard xmlns:vcard="...">
#   <vcard:version>4.0</vcard:version>
#   <dc:title xmlns:dc="...">Contact: Dr. John Doe</dc:title>
#   <dcterms:created xmlns:dcterms="...">2024-06-01T12:00:00Z</dcterms:created>
# </vcard:vCard>

When NOT to use namespace_scope

Single namespace documents:

# Don't use namespace_scope for single namespace
class Ceramic < Lutaml::Model::Serializable
  xml do
    element 'ceramic'
    namespace CeramicNamespace
    # namespace_scope [CeramicNamespace]  # NOT NEEDED - only one namespace

    map_element 'type', to: :type
    map_element 'glaze', to: :glaze
  end
end

Rarely used namespaces:

# Keep rarely-used namespaces local for clarity
class Document < Lutaml::Model::Serializable
  xml do
    root 'document'
    namespace DocumentNamespace
    namespace_scope [DocumentNamespace]  # Only frequently used

    map_element 'title', to: :title
    map_element 'content', to: :content
    # Special metadata element used rarely
    map_element 'metadata', to: :metadata  # MetadataNamespace declared locally
  end
end

Element declaration practices

Use element() for new code

Do:

class Model < Lutaml::Model::Serializable
  xml do
    element 'model'  # Clear, modern API
    map_element 'attr', to: :attr
  end
end

Acceptable:

class Model < Lutaml::Model::Serializable
  xml do
    root 'model'  # Backward compatible, still works
    map_element 'attr', to: :attr
  end
end

Omit element declaration for type-only models

Do:

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

  xml do
    # No element() - embedded type only
    sequence do
      map_element 'street', to: :street
      map_element 'city', to: :city
    end
  end
end

Don’t:

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

Mixed content practices

Use explicit mixed_content() for clarity

Do:

class RichText < Lutaml::Model::Serializable
  xml do
    element 'text'
    mixed_content  # Explicit and clear
    map_element 'b', to: :bold
  end
end

Acceptable:

class RichText < Lutaml::Model::Serializable
  xml do
    root 'text', mixed: true # Also works
    map_element 'b', to: :bold
  end
end

Sequence practices

Use sequence when order matters

Do:

class Person < Lutaml::Model::Serializable
  xml do
    element 'person'
    sequence do  # Enforce strict order
      map_element 'firstName', to: :first_name
      map_element 'lastName', to: :last_name
      map_element 'email', to: :email
    end
  end
end

Don’t:

# Without sequence when order is important per XSD
class Person < Lutaml::Model::Serializable
  xml do
    element 'person'
    # Elements can appear in any order - may not match XSD
    map_element 'firstName', to: :first_name
    map_element 'lastName', to: :last_name
  end
end

XSD generation practices

Add documentation for generated schemas

Do:

class ContactNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/contact/v1'
  version '1.0'
  documentation "Contact information schema for Example Corp"
end

class Contact < Lutaml::Model::Serializable
  xml do
    element 'contact'
    namespace ContactNamespace
    documentation "Represents a contact record"
    type_name 'ContactRecordType'
  end
end

Don’t:

# Missing documentation makes generated XSD less useful
class Contact < Lutaml::Model::Serializable
  xml do
    element 'contact'
  end
end

Use explicit xsd_type for special cases

Do:

class Product < Lutaml::Model::Serializable
  attribute :product_id, :string, xsd_type: 'xs:ID'
  attribute :parent_ref, :string, xsd_type: 'xs:IDREF'

  xml do
    element 'product'
    map_attribute 'id', to: :product_id
    map_attribute 'parentRef', to: :parent_ref
  end
end

Don’t:

# Rely on default inference for ID/IDREF relationships
class Product < Lutaml::Model::Serializable
  attribute :product_id, :string  # Won't generate xs:ID
  attribute :parent_ref, :string  # Won't generate xs:IDREF
end

Qualification practices

Be consistent within a schema

Do:

class MyNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/schema'
  element_form_default :qualified  # Set once, applies to all
end

# All models using this namespace follow the same rules
class Model1 < Lutaml::Model::Serializable
  xml do
    element 'model1'
    namespace MyNamespace
  end
end

class Model2 < Lutaml::Model::Serializable
  xml do
    element 'model2'
    namespace MyNamespace
  end
end

Don’t:

# Inconsistent qualification creates confusion
class Model1 < Lutaml::Model::Serializable
  xml do
    element 'model1'
    namespace 'https://example.com/schema', 'pfx'
    map_element 'attr', to: :attr, form: :qualified
  end
end

class Model2 < Lutaml::Model::Serializable
  xml do
    element 'model2'
    namespace 'https://example.com/schema', 'pfx'
    map_element 'attr', to: :attr, form: :unqualified
  end
end

Use form: sparingly

Do:

# Set namespace-level defaults, override only when needed
class MyNamespace < Lutaml::Model::XmlNamespace
  element_form_default :qualified
end

class Model < Lutaml::Model::Serializable
  xml do
    namespace MyNamespace
    map_element 'special', to: :special, form: :unqualified  # Exception
    map_element 'normal', to: :normal  # Uses default
  end
end

Don’t:

# Overriding form on every element defeats namespace defaults
class Model < Lutaml::Model::Serializable
  xml do
    namespace MyNamespace
    map_element 'attr1', to: :attr1, form: :qualified
    map_element 'attr2', to: :attr2, form: :qualified
    map_element 'attr3', to: :attr3, form: :qualified
    # Just set element_form_default :qualified instead!
  end
end

Encoding practices

Set encoding explicitly for non-UTF-8

Do:

# Per-instance for consistent encoding
instance = JapaneseCeramic.new(glaze_type: "志野釉")
instance.encoding = "Shift_JIS"
xml = instance.to_xml

# Or per-export when needed
xml = instance.to_xml(encoding: "Shift_JIS")

Don’t:

# Rely on defaults for non-UTF-8 data
instance = JapaneseCeramic.new(glaze_type: "志野釉")
xml = instance.to_xml  # May produce corrupted output with some adapters

Testing practices

Test round-trip serialization

Do:

RSpec.describe Contact do
  it "round-trips correctly" do
    original = Contact.new(name: "John", email: "john@example.com")
    xml = original.to_xml
    parsed = Contact.from_xml(xml)

    expect(parsed).to eq(original)
    expect(parsed.name).to eq("John")
    expect(parsed.email).to eq("john@example.com")
  end
end

Test namespace presence

Do:

RSpec.describe Contact do
  it "includes correct namespace" do
    contact = Contact.new(name: "John")
    xml = contact.to_xml

    expect(xml).to include('xmlns:contact=')
    expect(xml).to include('https://example.com/schemas/contact/v1')
  end
end

Test XSD generation if using

Do:

RSpec.describe "XSD generation" do
  it "generates valid XSD" do
    xsd = Lutaml::Model::Schema.to_xml(Contact)

    expect(xsd).to include('<xs:schema')
    expect(xsd).to include('targetNamespace')
    expect(xsd).to include('ContactRecordType')
  end
end

Performance practices

Choose appropriate adapter

XML adapters:

  • Ox: Fastest, use for performance-critical applications

  • Nokogiri: Most compatible, good balance

  • Oga: Pure Ruby, works with Opal, use when no native extensions allowed

Configure once:

Lutaml::Model::Config.configure do |config|
  config.xml_adapter_type = :ox  # or :nokogiri, :oga
end

Avoid excessive nesting

Do:

# Flat structure when possible
class Address < Lutaml::Model::Serializable
  attribute :street, :string
  attribute :city, :string
  attribute :postal_code, :string
end

Avoid when unnecessary:

# Unnecessary nesting
class Street < Lutaml::Model::Serializable
  attribute :name, :string
end

class City < Lutaml::Model::Serializable
  attribute :name, :string
end

class Address < Lutaml::Model::Serializable
  attribute :street, Street
  attribute :city, City
  # Just use :string attributes directly
end

Organization practices

Do:

# lib/my_project/namespaces.rb
module MyProject
  module Namespaces
    class Contact < Lutaml::Model::XmlNamespace
      uri 'https://example.com/contact/v1'
      prefix_default 'contact'
    end

    class Address < Lutaml::Model::XmlNamespace
      uri 'https://example.com/address/v1'
      prefix_default 'addr'
    end
  end
end

Separate models by domain

Do:

# lib/my_project/models/contact.rb
class Contact < Lutaml::Model::Serializable
  # ...
end

# lib/my_project/models/address.rb
class Address < Lutaml::Model::Serializable
  # ...
end

Documentation practices

Document complex mappings

Do:

class ComplexModel < Lutaml::Model::Serializable
  # This attribute maps to either 'type' or 'kind' element
  # depending on schema version
  attribute :type_info, :string

  xml do
    element 'model'
    # Map multiple names for backward compatibility
    map_element ['type', 'kind'], to: :type_info
  end
end

Add XSD documentation

Do:

class Contact < Lutaml::Model::Serializable
  xml do
    element 'contact'
    documentation "Represents a contact with name and email"

    map_element 'name', to: :name
    map_element 'email', to: :email
  end
end

Error handling practices

Validate after parsing

Do:

xml = get_xml_from_source()
contact = Contact.from_xml(xml)
contact.validate!  # Raises if invalid

# Or silent validation
errors = contact.validate
if errors.any?
  handle_errors(errors)
end

Use appropriate error types

Do:

begin
  Contact.from_xml(invalid_xml)
rescue Lutaml::Model::InvalidFormatError => e
  # Handle parse errors
rescue Lutaml::Model::ValidationError => e
  # Handle validation errors
rescue Lutaml::Model::NoRootMappingError => e
  # Handle type-only model parsing attempts
end

Schema versioning practices

Include version in namespace URI

Do:

class ContactNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/schemas/contact/v1'  # Version in URI
  version '1.0'  # Also track in metadata
end

Support multiple versions

Do:

class ContactNamespaceV1 < Lutaml::Model::XmlNamespace
  uri 'https://example.com/schemas/contact/v1'
  version '1.0'
end

class ContactNamespaceV2 < Lutaml::Model::XmlNamespace
  uri 'https://example.com/schemas/contact/v2'
  version '2.0'
end

# Models can explicitly choose version
class LegacyContact < Lutaml::Model::Serializable
  xml do
    element 'contact'
    namespace ContactNamespaceV1
  end
end

class ModernContact < Lutaml::Model::Serializable
  xml do
    element 'contact'
    namespace ContactNamespaceV2
  end
end

Code style practices

Follow consistent indentation

Do:

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

  xml do
    element 'model'
    namespace MyNamespace

    sequence do
      map_element 'name', to: :name
    end
  end
end

Do:

class Model < Lutaml::Model::Serializable
  xml do
    element 'model'
    namespace MyNamespace

    # Attributes first
    map_attribute 'id', to: :id
    map_attribute 'version', to: :version

    # Then elements in logical order
    sequence do
      map_element 'name', to: :name
      map_element 'description', to: :description
    end
  end
end

Summary checklist

When writing XML mappings:

  • ☑ Use XmlNamespace classes for projects with multiple models

  • ☑ Use element() for new code (or root() for backward compat)

  • ☑ Omit element declaration for type-only models (no no_root)

  • ☑ Set element_form_default :qualified for complex schemas

  • ☑ Keep attribute_form_default :unqualified (W3C convention)

  • ☑ Use sequence when element order is significant

  • ☑ Add documentation for XSD generation

  • ☑ Specify xsd_type for ID/IDREF and custom XSD types

  • ☑ Use Type-level namespaces for reusable types across models

  • ☑ Test round-trip serialization (both to_xml and from_xml)

  • ☑ Validate parsed data with validate or validate!

  • ☑ Set encoding explicitly for non-UTF-8 data

  • ☑ Choose appropriate XML adapter for your needs

  • ☑ Follow namespace resolution priority (explicit > type > inherit > form default)