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
    element "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
    element "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
    element "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
    element "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
    element "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) — round-trip example
class MathExpression < Lutaml::Model::Serializable
  attribute :content, :string

  xml do
    element "math"
    map_content to: :content
  end
end

class Paragraph < Lutaml::Model::Serializable
  attribute :text, :string, collection: true     # collection: true required
  attribute :bold, :string, collection: true
  attribute :stem, MathExpression, collection: true

  xml do
    element 'p'
    mixed_content  # Enable mixed content + ordered round-trip

    map_content to: :text
    map_element 'bold', to: :bold
    map_element 'stem', to: :stem
  end
end
<p>Temperature <stem>T</stem> minus <stem>T<sub>90</sub></stem></p>
parsed = Paragraph.from_xml(xml)

# Access text segments and elements separately
parsed.text   # => ["Temperature ", " minus "]
parsed.stem.map(&:content)  # => ["T", "T90"]

# Serialize back -- text and elements appear in their original positions
parsed.to_xml
# => "<p>Temperature <stem>T</stem> minus <stem>T<sub>90</sub></stem></p>"

# Iterate in document order to process text and elements interleaved
parsed.each_mixed_content do |node|
  case node
  when String
    puts "Text: #{node.inspect}"
  when MathExpression
    puts "Math: #{node.content}"
  end
end
# Output:
#   Text: "Temperature "
#   Math: T
#   Text: " minus "
#   Math: T90
map_content requires collection: true for mixed content

For mixed content, map_content must target an attribute with collection: true because XML like:

<hello><element1>hi</element1> ho <element2>he</element2>? Yes!</hello>

has multiple text segments between elements. Each segment becomes an entry in the content collection.

Without mixed_content, serialization groups all text first then all elements (or vice versa), losing the original interleaving. This is the most common cause of XML round-trip failures in document-processing applications.

=== Builder interface for mixed content

When constructing models with mixed_content, the builder interface provides a DSL for recording insertion order. The builder tracks element order via element_order for serialization.

==== Model definitions

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

  xml do
    element "person"
    map_content to: :name
  end
end

class Group < Lutaml::Model::Serializable
  attribute :member, Person, collection: true
  attribute :member_key, :string, collection: true
  attribute :description, :string, collection: true

  xml do
    element "group"
    mixed_content
    map_element "member", to: :member
    map_element "member_key", to: :member_key
    map_content to: :description
  end
end

==== NO MAGIC: Explicit receiver

group = Group.new do |g|
  members.each do |m|
    g.member(m[:person])
    g.member_key(m[:key])
    g.description("\n  " + m[:description] + "\n")
  end
end

==== MAGIC: instance_eval (no receiver)

group = Group.new do
  members.each do |m|
    member(m[:person])
    member_key(m[:key])
    description("\n  " + m[:description] + "\n")
  end
end

The element_order array records entries in the order they were added, preserving the sequence for XML serialization.

Why whitespace is intentional content

In mixed content XML, whitespace between elements is content:

<group>
  <member>John Hodges</member>
  <member_key>john</member_key>
  He is a good man.
  <member>Thomas Mellon</member>
  <member_key>thomas</member_key>
  He was a strong man.
</group>

The builder records whitespace strings as first-class content entries:

description("\n  " + m[:description] + "\n")
#          ↑                            ↑
#     before whitespace          after whitespace

=== Ordered content

class RootOrderedContent < Lutaml::Model::Serializable
  attribute :bold, :string
  attribute :italic, :string
  attribute :underline, :string

  xml do
    element "RootOrderedContent"
    ordered  # Preserve element order
    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>

=== Iterating mixed content with each_mixed_content

When working with mixed content (text interspersed with elements) or ordered content (elements in a specific sequence), you often need to iterate in document order to process or transform the content. The each_mixed_content method provides an iterator that yields text nodes and typed model objects in their original document order.

each_mixed_content is available on any model instance that has mixed_content or ordered enabled in its XML mapping. It returns an empty Enumerator if neither is set.

class Docbook::Elements::Para < Lutaml::Model::Serializable
  attribute :content, :string
  attribute :emphasis, :string, collection: true
  attribute :code, :string, collection: true

  xml do
    element "para"
    mixed_content
    map_content to: :content
    map_element "emphasis", to: :emphasis
    map_element "code", to: :code
  end
end
<para>Hello <emphasis role="bold">world</emphasis> and <code>example</code>!</para>
para = Docbook::Elements::Para.from_xml(xml)

# Iterate in document order, yielding text and elements
para.each_mixed_content do |node|
  case node
  when String
    puts "Text: #{node.inspect}"
  when String  # emphasis/code are stored as strings in this model
    puts "Inline element content: #{node.inspect}"
  end
end
# => "Text: \"Hello \""
# => "Inline element content: \"world\""
# => "Text: \" and \""
# => "Inline element content: \"example\""
# => "Text: \"!\""

A common use case is converting DocBook or similar XML to HTML:

class HtmlConverter
  def convert_para(para)
    html = "<p>"

    para.each_mixed_content do |node|
      case node
      when String
        html += escape_html(node)
      when Docbook::Elements::Emphasis
        role = node.role == "bold" ? "strong" : "em"
        html += "<#{role}>#{escape_html(node.content)}</#{role}>"
      end
    end

    html += "</p>"
    html
  end

  private

  def escape_html(text)
    text.gsub("&", "&amp;")
         .gsub("<", "&lt;")
         .gsub(">", "&gt;")
  end
end
Return value and chaining

each_mixed_content returns:

  • self when called with a block (for chaining)

  • An Enumerator when called without a block — supports with_index, select, map, etc.

# With block
para.each_mixed_content { |node| process(node) }

# Without block - returns Enumerator
enum = para.each_mixed_content
enum.select { |node| node.is_a?(String) }
enum.each { |node| process(node) }

# With index - useful for positional processing
para.each_mixed_content.with_index do |node, index|
  puts "#{index}: #{node.inspect}"
end
# 0: "Temperature "
# 1: #<MathExpression content="T">
# 2: " minus "
# 3: #<MathExpression content="T90">

Whitespace-only text fragments are automatically skipped by each_mixed_content. This matches typical XML processing behavior where insignificant whitespace is ignored.

For JSON and YAML formats, rich text uses a recursive tree pattern instead of the mixed content model used by XML. Each node in the tree knows its type and contains its own children. Since the tree IS the content structure, you can iterate using natural tree traversal methods.

See the next section for details.

=== Recursive tree pattern for JSON/YAML

Unlike XML’s mixed content model (which requires each_mixed_content to correlate text fragments with element collections), JSON and YAML represent rich text using a recursive tree structure. Each node knows its type, and container nodes hold a children array containing child nodes.

XML mixed content and JSON/YAML recursive tree are fundamentally different data structures. They cannot be represented by the same Ruby model class because:

  • XML: content is a String, emphasis is a collection

  • JSON/YAML: content is a collection of nodes, each with children

Instead, define separate models for each format family.

Recursive tree structure for rich text
{
  "type": "para",
  "content": [
    { "type": "text", "value": "Hello " },
    { "type": "emphasis", "role": "bold", "children": [
      { "type": "text", "value": "world" }
    ]},
    { "type": "text", "value": "!" }
  ]
}

In this pattern: - Each node has a type field indicating what kind of node it is - Container nodes (like emphasis) have a children array - Leaf nodes (like text) have a value field - The tree structure itself preserves order - no separate tracking needed

For XML, use mixed_content with separate content attribute and element collections:

class Docbook::Elements::Para < Lutaml::Model::Serializable
  attribute :content, :string
  attribute :emphasis, Docbook::Elements::Emphasis, collection: true

  xml do
    element "para"
    mixed_content
    map_content to: :content
    map_element "emphasis", to: :emphasis
    xml_space :preserve
  end
end

class Docbook::Elements::Emphasis < Lutaml::Model::Serializable
  attribute :role, :string
  attribute :content, :string

  xml do
    element "emphasis"
    map_attribute "role", to: :role
    map_content to: :content
  end
end
<para>Hello <emphasis role="bold">world</emphasis>!</para>

For JSON/YAML, use a polymorphic node structure where inline elements have children:

# Base class for all rich text nodes
class RichText::Node < Lutaml::Model::Serializable
end

# Leaf node for text
class RichText::TextNode < Lutaml::Model::Serializable
  attribute :value, :string

  json do
    map "value", to: :value
  end

  yaml do
    map "value", to: :value
  end
end

# Inline element with children
class RichText::Emphasis < Lutaml::Model::Serializable
  attribute :role, :string
  attribute :children, RichText::Node, collection: true

  json do
    map "type", to: :type
    map "role", to: :role
    map "children", to: :children
  end

  yaml do
    map "type", to: :type
    map "role", to: :role
    map "children", to: :children
  end

  def type
    "emphasis"
  end
end

# Container with content
class RichText::Para < Lutaml::Model::Serializable
  attribute :type, :string
  attribute :content, RichText::Node, collection: true

  json do
    map "type", to: :type
    map "content", to: :content
  end

  yaml do
    map "type", to: :type
    map "content", to: :content
  end

  def type
    "para"
  end
end
{
  "type": "para",
  "content": [
    { "type": "text", "value": "Hello " },
    { "type": "emphasis", "role": "bold", "children": [
      { "type": "text", "value": "world" }
    ]},
    { "type": "text", "value": "!" }
  ]
}
Processing rich text by format
# XML: Use each_mixed_content
xml_para = Docbook::Elements::Para.from_xml(xml_input)
xml_para.each_mixed_content do |node|
  case node
  when String
    print node  # Text fragments
  when Docbook::Elements::Emphasis
    print "<#{node.role}>#{node.content}</#{node.role}>"
  end
end

# JSON/YAML: Use natural tree traversal
json_para = RichText::Para.from_json(json_input)

def process_node(node)
  case node
  when RichText::TextNode
    print node.value
  when RichText::Emphasis
    print "<#{node.role}>"
    node.children.each { |child| process_node(child) }
    print "</#{node.role}>"
  end
end

json_para.content.each { |node| process_node(node) }
Table 1. Comparison: XML vs JSON/YAML for rich text
Aspect XML Mixed Content JSON/YAML Recursive Tree

Ruby model attributes

content (String), emphasis (collection)

content (collection of Nodes), children (collection)

Text storage

Single content string attribute

Separate TextNode objects with value

Element nesting

Flat - elements stored in collections

Recursive - elements have children array

Order tracking

Via element_order + each_mixed_content

Implicit in tree structure

Iteration method

each_mixed_content

`node.children.each {

child

…​ }`

Whitespace

Dual-format serialization: Same content, different models

These are separate model classes by design, not one model that does both. Each format family (XML vs JSON/YAML) requires a different data structure:

# ============================================
# XML MODEL: Mixed Content Pattern
# ============================================
# Structure: content (String) + emphasis (collection)
# Use each_mixed_content to iterate in document order

class Docbook::Elements::Para < Lutaml::Model::Serializable
  attribute :content, :string
  attribute :emphasis, Docbook::Elements::Emphasis, collection: true

  xml do
    element "para"
    mixed_content
    map_content to: :content
    map_element "emphasis", to: :emphasis
    xml_space :preserve
  end
end

class Docbook::Elements::Emphasis < Lutaml::Model::Serializable
  attribute :role, :string
  attribute :content, :string

  xml do
    element "emphasis"
    map_attribute "role", to: :role
    map_content to: :content
  end
end

# ============================================
# JSON/YAML MODEL: Recursive Tree Pattern
# ============================================
# Structure: content (collection of Nodes) + children (in each node)
# Use natural tree traversal: node.children.each { ... }

class RichText::Node < Lutaml::Model::Serializable
  # Base class for polymorphic node types
end

class RichText::TextNode < Lutaml::Model::Serializable
  attribute :type, :string
  attribute :value, :string

  def type
    "text"
  end
end

class RichText::Emphasis < Lutaml::Model::Serializable
  attribute :type, :string
  attribute :role, :string
  attribute :children, RichText::Node, collection: true

  def type
    "emphasis"
  end
end

class RichText::Para < Lutaml::Model::Serializable
  attribute :type, :string
  attribute :content, RichText::Node, collection: true

  def type
    "para"
  end
end

# Define JSON/YAML mappings separately
class RichText::TextNode
  json do map "value", to: :value end
  yaml do map "value", to: :value end
end

class RichText::Emphasis
  json do
    map "type", to: :type
    map "role", to: :role
    map "children", to: :children
  end
  yaml do
    map "type", to: :type
    map "role", to: :role
    map "children", to: :children
  end
end

class RichText::Para
  json do
    map "type", to: :type
    map "content", to: :content
  end
  yaml do
    map "type", to: :type
    map "content", to: :content
  end
end
Serialization examples
# XML: Build model and serialize
xml_para = Docbook::Elements::Para.new(
  content: "Hello ",
  emphasis: [
    Docbook::Elements::Emphasis.new(role: "bold", content: "world")
  ]
)
xml_para.content += "!"  # Append to string

puts xml_para.to_xml
# => <para xml:space="preserve">Hello <emphasis role="bold">world</emphasis>!</para>

# JSON: Build model and serialize
json_para = RichText::Para.new(
  content: [
    RichText::TextNode.new(value: "Hello "),
    RichText::Emphasis.new(
      role: "bold",
      children: [RichText::TextNode.new(value: "world")]
    ),
    RichText::TextNode.new(value: "!")
  ]
)

puts json_para.to_json
# => {"type":"para","content":[
#      {"type":"text","value":"Hello "},
#      {"type":"emphasis","role":"bold","children":[
#        {"type":"text","value":"world"}
#      ]},
#      {"type":"text","value":"!"}
#    ]}

# YAML also works:
puts json_para.to_yaml
Whitespace handling for XML

For XML mixed content, use xml_space :preserve to maintain whitespace:

class Docbook::Elements::Para < Lutaml::Model::Serializable
  attribute :content, :string
  attribute :emphasis, Docbook::Elements::Emphasis, collection: true

  xml do
    element "para"
    mixed_content
    xml_space :preserve  # <-- Critical for rich text
    map_content to: :content
    map_element "emphasis", to: :emphasis
  end
end

== Sequence patterns

=== Basic sequence

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

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

# 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

class Example < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :description, :string
  attribute :title, :string
  attribute :note, :string

  xml do
    element "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

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

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

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

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

class CeramicCollection < Lutaml::Model::Serializable
  attribute :items, Ceramic, collection: true

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

=== Attribute collections with delimiter

class TitleCollection < Lutaml::Model::Collection
  instances :items, :string

  xml do
    element "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

class JapaneseCeramic < Lutaml::Model::Serializable
  attribute :glaze_type, :string
  attribute :description, :string

  xml do
    element "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

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

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

  xml do
    # Type-only model - no element declaration needed

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

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

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

# 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

# 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
    element "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

# 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
    element "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

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