Basic element and attribute mapping
Simple model with elements
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
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
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
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
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
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
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, has multiple text segments between elements. Each segment becomes an entry in the content collection. Without === Builder interface for mixed content When constructing models with ==== Model definitions ==== 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
endThe 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("&", "&")
.gsub("<", "<")
.gsub(">", ">")
end
endeach_mixed_content returns:
-
selfwhen called with a block (for chaining) -
An
Enumeratorwhen called without a block — supportswith_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:
contentis a String,emphasisis a collection -
JSON/YAML:
contentis a collection of nodes, each withchildren
Instead, define separate models for each format family.
{
"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": "!" }
]
}# 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) }| Aspect | XML Mixed Content | JSON/YAML Recursive Tree |
|---|---|---|
Ruby model attributes |
|
|
Text storage | Single | Separate |
Element nesting | Flat - elements stored in collections | Recursive - elements have |
Order tracking | Via | Implicit in tree structure |
Iteration method |
| `node.children.each { |
child | … }` | Whitespace |
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# 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_yamlFor 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 & Jane</potter> A ∑ series...</ceramic>
# Using ASCII encoding
ceramic_instance.to_xml(encoding: "ASCII")
# => <ceramic><potter>John & Jane</potter> A ∑ 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