Basic element and attribute mapping

Simple model with elements

Example 1. Basic ceramic model with element mappings
class Ceramic < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :description, :string
  attribute :temperature, :integer

  xml do
    root 'ceramic'
    map_element 'name', to: :name
    map_attribute 'temperature', to: :temperature
    map_content to: :description
  end
end
<ceramic temperature="1200">
  <name>Porcelain Vase</name>
  with celadon glaze.
</ceramic>
> Ceramic.from_xml(xml)
> #<Ceramic @name="Porcelain Vase",
   @description=" with celadon glaze.",
   @temperature=1200>

Nested models

Example 2. Model with nested object
class Glaze < Lutaml::Model::Serializable
  attribute :color, :string
  attribute :temperature, :integer

  xml do
    root 'Glaze'
    map_element 'color', to: :color
    map_element 'temperature', to: :temperature
  end
end

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

  xml do
    root 'Ceramic'
    map_element 'Type', to: :type
    map_element 'Glaze', to: :glaze
  end
end
<Ceramic>
  <Type>Porcelain</Type>
  <Glaze>
    <color>Clear</color>
    <temperature>1050</temperature>
  </Glaze>
</Ceramic>

Namespace patterns

Single namespace

Example 3. Model with single namespace
class Ceramic < Lutaml::Model::Serializable
  attribute :type, :string
  attribute :glaze, :string

  xml do
    root 'Ceramic'
    namespace 'http://example.com/ceramic'
    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>

Prefixed namespace

Example 4. Model with prefixed namespace
class Ceramic < Lutaml::Model::Serializable
  attribute :type, :string
  attribute :glaze, :string

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

Multi-namespace with XmlNamespace class

Example 5. Model using multiple namespaces with XmlNamespace classes
class CeramicNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/ceramic'
  prefix_default 'cer'
end

class GlazeNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/glaze'
  prefix_default 'glz'
end

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

  xml do
    element 'ceramic'
    namespace CeramicNamespace

    map_element 'type', to: :type
    map_element 'glaze', to: :glaze,
      namespace: GlazeNamespace
  end
end
<cer:ceramic xmlns:cer="https://example.com/ceramic"
             xmlns:glz="https://example.com/glaze">
  <type>Porcelain</type>
  <glz:glaze>Celadon</glz:glaze>
</cer:ceramic>

Namespace inheritance pattern

Example 6. Element explicitly inheriting parent namespace
class CeramicNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/ceramic'
  prefix_default 'cer'
  element_form_default :unqualified
end

class SpecialType < Lutaml::Model::Serializable
  attribute :value, :string

  xml do
    element 'specialType'
    namespace CeramicNamespace  # Inherit parent namespace
    map_content to: :value
  end
end

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

  xml do
    element 'ceramic'
    namespace CeramicNamespace

    map_element 'type', to: :type  # Unqualified (follows element_form_default)
    map_element 'specialType', to: :special_type  # Uses SpecialType's namespace
  end
end
<cer:ceramic xmlns:cer="https://example.com/ceramic">
  <type>Porcelain</type>
  <cer:specialType>Fine</cer:specialType>
</cer:ceramic>

Rich content patterns

Mixed content

Example 7. Model with mixed content (text + elements)
class Paragraph < Lutaml::Model::Serializable
  attribute :bold, :string, collection: true
  attribute :italic, :string

  xml do
    root 'p', mixed: true
    map_element 'bold', to: :bold
    map_element 'i', to: :italic
  end
end
<p>My name is <bold>John Doe</bold>, and I'm <i>28</i> years old</p>
> Paragraph.from_xml(xml)
> #<Paragraph @bold="John Doe", @italic="28">

Ordered content

Example 8. Model preserving element order
class RootOrderedContent < Lutaml::Model::Serializable
  attribute :bold, :string
  attribute :italic, :string
  attribute :underline, :string

  xml do
    root "RootOrderedContent", ordered: true
    map_element :bold, to: :bold
    map_element :italic, to: :italic
    map_element :underline, to: :underline
  end
end
<RootOrderedContent>
  <underline>Moon</underline>
  <italic>384,400 km</italic>
  <bold>bell</bold>
</RootOrderedContent>

When serialized back, the order is preserved:

> instance = RootOrderedContent.from_xml(xml)
> instance.to_xml
> #<RootOrderedContent>
    <underline>Moon</underline>
    <italic>384,400 km</italic>
    <bold>bell</bold>
  </RootOrderedContent>

Sequence patterns

Basic sequence

Example 9. Model with strict element ordering
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
<collection>
  <kiln>
    <id>1</id>
    <name>Nick</name>
    <type>Hard</type>
    <color>Black</color>
  </kiln>
</collection>

If elements appear out of order, an error is raised.

Sequence with namespace

Example 10. Combining sequence with namespace and XmlNamespace class
class ContactNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/schemas/contact/v1'
  prefix_default 'contact'
  element_form_default :qualified
end

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
<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>

Type-only models (no element)

Embedded type pattern

Example 11. Type-only model used within parent
# Type-only model - no element() or root() call
class Address < Lutaml::Model::Serializable
  attribute :street, :string
  attribute :city, :string
  attribute :postal_code, :string

  xml do
    # No element declaration - this is a type-only model
    sequence do
      map_element 'street', to: :street
      map_element 'city', to: :city
      map_element 'postalCode', to: :postal_code
    end
  end
end

# Parent model using the type
class Contact < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :address, Address

  xml do
    element 'contact'

    sequence do
      map_element 'name', to: :name
      map_element 'address', to: :address
    end
  end
end
<contact>
  <name>John Doe</name>
  <address>
    <street>123 Main St</street>
    <city>Metropolis</city>
    <postalCode>12345</postalCode>
  </address>
</contact>

CDATA patterns

Forcing CDATA output

Example 12. Using cdata option to preserve special characters
class Example < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :description, :string
  attribute :title, :string
  attribute :note, :string

  xml do
    root 'example'
    map_element :name, to: :name, cdata: true
    map_content to: :description, cdata: true
    map_element :title, to: :title, cdata: false
    map_element :note, to: :note, cdata: false
  end
end
<example>
  <name><![CDATA[John]]></name>
  <![CDATA[here is the description]]>
  <title>Lutaml</title>
  <note>Careful</note>
</example>

XSD generation patterns

Basic XSD generation

Example 13. Generating XSD from model
class Contact < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :email, :string

  xml do
    element 'contact'
    map_element 'name', to: :name
    map_element 'email', to: :email
  end
end

# Generate XSD
xsd = Lutaml::Model::Schema.to_xml(Contact)
puts xsd

XSD with namespace and documentation

Example 14. Complete XSD generation with metadata
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
  version '1.0'
  documentation "Contact information schema"
end

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

  xml do
    element 'contact'
    namespace ContactNamespace
    documentation "A contact record"

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

# Generate XSD with options
xsd = Lutaml::Model::Schema.to_xml(
  Contact,
  namespace: ContactNamespace.uri,
  prefix: ContactNamespace.prefix_default,
  output_dir: 'schemas',
  create_files: true
)

Qualification patterns

Qualified elements pattern

Example 15. All elements namespace-qualified
class CeramicNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/ceramic'
  prefix_default 'cer'
  element_form_default :qualified  # All local elements qualified
end

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

  xml do
    element 'ceramic'
    namespace CeramicNamespace

    map_element 'type', to: :type
    map_element 'color', to: :color
  end
end
<cer:ceramic xmlns:cer="https://example.com/ceramic">
  <cer:type>Porcelain</cer:type>
  <cer:color>White</cer:color>
</cer:ceramic>

Selective qualification

Example 16. Override qualification for specific elements
class CeramicNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/ceramic'
  prefix_default 'cer'
  element_form_default :unqualified  # Default: unqualified
end

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

  xml do
    element 'ceramic'
    namespace CeramicNamespace

    # Override to qualified
    map_element 'type', to: :type, form: :qualified
    # Use default (unqualified)
    map_element 'glaze', to: :glaze
    # Force attribute qualified
    map_attribute 'id', to: :id, form: :qualified
  end
end
<cer:ceramic xmlns:cer="https://example.com/ceramic" cer:id="C001">
  <cer:type>Porcelain</cer:type>
  <glaze>Clear</glaze>
</cer:ceramic>

Collection patterns

Element collections

Example 17. Model with element collection
class CeramicCollection < Lutaml::Model::Serializable
  attribute :items, Ceramic, collection: true

  xml do
    root 'ceramics'
    map_element 'ceramic', to: :items
  end
end
<ceramics>
  <ceramic>...</ceramic>
  <ceramic>...</ceramic>
  <ceramic>...</ceramic>
</ceramics>

Attribute collections with delimiter

Example 18. XML attribute containing delimited values
class TitleCollection < Lutaml::Model::Collection
  instances :items, :string

  xml do
    root "titles"
    map_attribute "title", to: :items, delimiter: "; "
  end
end
<titles title="Title One; Title Two; Title Three"/>
collection = TitleCollection.from_xml(xml)
collection.items
# => ["Title One", "Title Two", "Title Three"]

Character encoding patterns

Per-instance encoding

Example 19. Setting encoding on model instance
class JapaneseCeramic < Lutaml::Model::Serializable
  attribute :glaze_type, :string
  attribute :description, :string

  xml do
    root 'JapaneseCeramic'
    map_attribute 'glazeType', to: :glaze_type
    map_element 'description', to: :description
  end
end

# Create instance with UTF-8 data
instance = JapaneseCeramic.new(
  glaze_type: "志野釉",
  description: "東京国立博物館コレクション"
)

# Set character encoding to Shift_JIS
instance.encoding = "Shift_JIS"

# Serialize with specified encoding
serialization_output = instance.to_xml

Per-export encoding

Example 20. Setting encoding during serialization
ceramic_instance = Ceramic.new(
  potter: "John & Jane",
  description: " A ∑ series of ∏ porcelain µ vases."
)

# Using default encoding of UTF-8
ceramic_instance.to_xml
# => <ceramic><potter>John &amp; Jane</potter> A ∑ series...</ceramic>

# Using ASCII encoding
ceramic_instance.to_xml(encoding: "ASCII")
# => <ceramic><potter>John &amp; Jane</potter> A &#8721; series...</ceramic>

Import patterns with sequence

Importing mappings in sequence

Example 21. Using import_model_mappings inside sequence
class Address < Lutaml::Model::Serializable
  attribute :street, :string
  attribute :city, :string
  attribute :zip, :string

  xml do
    no_root

    map_element :street, to: :street
    map_element :city, to: :city
    map_element :zip, to: :zip
  end
end

class Person < Lutaml::Model::Serializable
  attribute :name, :string
  import_model_attributes Address

  xml do
    root "Person"

    map_element :name, to: :name
    sequence do
      import_model_mappings Address
    end
  end
end
<Person>
  <name>John Doe</name>
  <street>123 Main St</street>
  <city>Metropolis</city>
  <zip>12345</zip>
</Person>

XSD type override patterns

Using xsd_type for IDs

Example 22. Specify XSD types for ID/IDREF
class Product < Lutaml::Model::Serializable
  attribute :product_id, :string, xsd_type: 'xs:ID'
  attribute :category_ref, :string, xsd_type: 'xs:IDREF'

  xml do
    element 'product'
    map_attribute 'id', to: :product_id
    map_attribute 'categoryRef', to: :category_ref
  end
end

# Generated XSD:
# <xs:attribute name="id" type="xs:ID"/>
# <xs:attribute name="categoryRef" type="xs:IDREF"/>

Using xsd_type for custom types

Example 23. Override automatic XSD type inference
class Document < Lutaml::Model::Serializable
  attribute :language, :string, xsd_type: 'xs:language'
  attribute :content_type, :string, xsd_type: 'xs:token'

  xml do
    element 'document'
    map_attribute 'lang', to: :language
    map_attribute 'contentType', to: :content_type
  end
end

Type-level namespace pattern

Type with own namespace

Example 24. Custom type declaring its namespace
# Define namespace for email types
class EmailNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/types/email'
  prefix_default 'email'
end

# Define type with namespace
class EmailType < Lutaml::Model::Type::String
  xml do
    namespace EmailNamespace  (1)
    xsd_type 'EmailAddress'   (2)
  end

  def self.cast(value)
    email = super(value)
    unless email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
      raise Lutaml::Model::TypeError, "Invalid email: #{email}"
    end
    email.downcase
  end
end

class Contact < Lutaml::Model::Serializable
  attribute :email, EmailType

  xml do
    element 'contact'
    namespace 'https://example.com/contact', 'c'

    map_element 'email', to: :email  # Uses EmailNamespace from EmailType
  end
end

contact = Contact.new(email: "USER@EXAMPLE.COM")
puts contact.to_xml
# => <c:contact xmlns:c="https://example.com/contact"
#               xmlns:email="https://example.com/types/email">
#      <email:email>user@example.com</email:email>
#    </c:contact>

# Round-trip deserialization works
parsed = Contact.from_xml(contact.to_xml)
parsed.email  # => "user@example.com"
1 Namespace directive associates EmailNamespace with this type
2 XSD type name for schema generation

Multi-namespace document with Type namespaces

Example 25. Dublin Core metadata in document
# Define namespaces
class DocumentNamespace < Lutaml::Model::XmlNamespace
  uri 'https://example.com/document'
  prefix_default 'doc'
end

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

# Define DC types with namespaces
class DcTitleType < Lutaml::Model::Type::String
  xml do
    namespace DublinCoreNamespace
    xsd_type 'titleType'
  end
end

class DcCreatorType < Lutaml::Model::Type::String
  xml do
    namespace DublinCoreNamespace
    xsd_type 'creatorType'
  end
end

# Use in document model
class Document < Lutaml::Model::Serializable
  namespace DocumentNamespace

  attribute :title, DcTitleType
  attribute :creator, DcCreatorType
  attribute :content, :string

  xml do
    root 'document'

    map_element 'title', to: :title      # Uses DublinCoreNamespace
    map_element 'creator', to: :creator  # Uses DublinCoreNamespace
    map_element 'content', to: :content  # No type namespace
  end
end

doc = Document.new(
  title: 'Example Document',
  creator: 'John Doe',
  content: 'Document content'
)

puts doc.to_xml
# => <doc:document
#      xmlns:doc="https://example.com/document"
#      xmlns:dc="http://purl.org/dc/elements/1.1/">
#      <dc:title>Example Document</dc:title>
#      <dc:creator>John Doe</dc:creator>
#      <content>Document content</content>
#    </doc:document>

# Round-trip works correctly
parsed = Document.from_xml(doc.to_xml)
parsed == doc  # => true

Multi-namespace document with consolidated declarations

Example 26. Using namespace_scope for cleaner multi-namespace XML
# 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

# Define types with namespaces
class DcTitleType < Lutaml::Model::Type::String
  xml do
    namespace DcNamespace
  end
end

class DctermsCreatedType < Lutaml::Model::Type::DateTime
  xml do
    namespace DctermsNamespace
  end
end

class VcardVersion < Lutaml::Model::Type::String
  xml do
    namespace VcardNamespace
  end
end

# Use namespace_scope to consolidate declarations
class Vcard < Lutaml::Model::Serializable
  namespace VcardNamespace

  attribute :version, VcardVersion
  attribute :title, DcTitleType
  attribute :full_name, :string
  attribute :created, DctermsCreatedType

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

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

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

puts vcard.to_xml
# => <vcard:vCard xmlns:vcard="urn:ietf:params:xml:ns:vcard-4.0"
#                 xmlns:dc="http://purl.org/dc/elements/1.1/"
#                 xmlns:dcterms="http://purl.org/dc/terms/">
#      <vcard:version>4.0</vcard:version>
#      <dc:title>Contact: Dr. John Doe</dc:title>
#      <vcard:fn>Dr. John Doe</vcard:fn>
#      <dcterms:created>2024-06-01T12:00:00+00:00</dcterms:created>
#    </vcard:vCard>

# Round-trip deserialization works
parsed = Vcard.from_xml(vcard.to_xml)
parsed == vcard  # => true
1 All three namespaces declared at root for cleaner output

schemaLocation pattern

Automatic xsi:schemaLocation

Example 27. Using schema_location metadata attribute
class CeramicNamespace < Lutaml::Model::XmlNamespace
  uri 'http://example.com/ceramic'
  prefix_default 'cera'
  element_form_default :qualified
end

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

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

  xml do
    root 'Ceramic'
    namespace CeramicNamespace
    map_element 'Type', to: :type  # Inherits parent namespace (qualified)
    map_element 'Glaze', to: :glaze
    map_attribute 'color', to: :color  # Per W3C, attributes don't inherit namespace
  end
end

xml_content = <<~XML
<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>
XML

c = Ceramic.from_xml(xml_content)
schema_loc = c.schema_location  # Automatically captured

# Round-trip with schema location preserved
new_c = Ceramic.new(
  type: "Porcelain",
  glaze: "Clear",
  color: "navy-blue",
  schema_location: schema_loc
)
puts new_c.to_xml
# xsi:schemaLocation automatically included