Overview
Version 0.8.0 is a major release that includes significant refactoring, new features, and breaking changes. This guide covers all migration topics in one place.
Key changes in v0.8.0:
-
Namespace restructuring - Format-specific code moved to dedicated namespaces
-
Configuration consolidation - Streamlined adapter configuration with sensible defaults
-
XML namespace behavior - Default namespace format, W3C-compliant defaults
-
TOML on Windows - tomlib disabled due to segfaults
-
Attribute name conflicts - Warnings only, no errors
-
DataModel transformation - Two-phase serialization pipeline for all formats
-
Consolidated
xml dosyntax - Unified syntax for models and Type classes -
XSD-aligned concepts - Element vs ComplexType distinction
-
Model-centric namespaces - Namespaces defined on models only, not mappings
-
Register system refactoring - Hierarchical fallbacks, composition, key-based registration
-
XSD schema generation - Generate XSD from models, parse XSD to models
Quick Start Migration
For most users, the migration is straightforward:
# Update your configuration to use simplified adapter names
Lutaml::Model::Config.configure do |config|
config.xml_adapter_type = :nokogiri # Now the default
config.json_adapter_type = :standard # Now the default
config.yaml_adapter_type = :standard # Now the default
config.toml_adapter_type = :toml_rb # Use :tomlib on non-Windows
config.hash_adapter_type = :standard # Now the default
endNamespace Restructuring
What Changed
Format-specific code has been moved to dedicated top-level namespaces:
| Old Namespace (v0.7.x) | New Namespace (v0.8.0) |
|---|---|
|
|
|
|
|
|
|
|
|
|
Adapter Class Locations
| Old Path (v0.7.x) | New Path (v0.8.0) |
|---|---|
|
|
|
|
|
|
|
|
Migration
If using explicit adapter class references, update the paths:
# OLD (v0.7.x)
require 'lutaml/model/xml/nokogiri_adapter'
config.xml_adapter = Lutaml::Model::Xml::NokogiriAdapter
# NEW (v0.8.0) - recommended approach
config.xml_adapter_type = :nokogiri
# NEW (v0.8.0) - if you need explicit class
require 'lutaml/xml/adapter/nokogiri_adapter'
config.xml_adapter = Lutaml::Xml::Adapter::NokogiriAdapterConfiguration Changes
TOML on Windows
The :tomlib adapter is disabled on Windows due to segmentation fault issues.
# On Windows, this now raises ArgumentError
config.toml_adapter_type = :tomlib # ❌ Error on Windows
# Use :toml_rb instead
config.toml_adapter_type = :toml_rb # ✅ Works on WindowsThe default is automatically set to :toml_rb on Windows.
XML Namespace Behavior Changes
Default Namespace Format
Version 0.8.0 changes the default XML output format from prefixed namespaces to default namespaces for cleaner, more standards-compliant XML.
Prefix Parameter Deprecation
The prefix parameter on XML mapping methods is deprecated. Define prefix_default in your XmlNamespace class instead.
Namespace Parameter Removal
The namespace: parameter on map_element, map_attribute, and map_all has been removed. Use typed attributes instead.
After (required)
# Create child model class with its own namespace
class Child < Lutaml::Model::Serializable
attribute :value, :string
xml do
element "Child"
namespace ChildNamespace # ✅ Namespace at model level
map_content to: :value
end
end
# Use typed attribute in parent
class Parent < Lutaml::Model::Serializable
attribute :child_data, Child # ✅ Changed type
xml do
element "Parent"
namespace ParentNamespace
map_element "Child", to: :child_data # ✅ No namespace parameter
end
endControlling XML Output Format
Version 0.8.0 introduces powerful options for controlling XML output format. Understanding these options is essential when your XML must conform to specific schema requirements or integrate with external systems.
Defining Prefixes with prefix_default
Prefixes are defined in your XmlNamespace class using prefix_default:
class PoNamespace < Lutaml::Xml::Namespace
uri "http://example.com/po"
prefix_default "po" # ✅ Define prefix here
element_form_default :qualified
end
class DcNamespace < Lutaml::Xml::Namespace
uri "http://purl.org/dc/elements/"
prefix_default "dc" # ✅ Define prefix here
end The prefix_default is the preferred prefix for the namespace. Whether it’s actually used depends on: 1. The output format you choose (to_xml options) 2. W3C rules (attributes require prefixes) 3. Whether children need the namespace |
XML Output Format Options
The to_xml method accepts several options to control namespace formatting:
Default Behavior (Default Namespace)
order.to_xmlProduces default namespace format (cleaner, W3C-preferred):
<PurchaseOrder xmlns="http://example.com/po">
<orderDate>2024-01-15</orderDate>
</PurchaseOrder>This is the new default in v0.8.0. No prefix is used; the namespace is declared as xmlns="…".
Force Prefix Format
order.to_xml(prefix: true)Produces prefixed format using the namespace’s prefix_default:
<po:PurchaseOrder xmlns:po="http://example.com/po">
<po:orderDate>2024-01-15</po:orderDate>
</po:PurchaseOrder>Use this when: - Consuming system requires prefixed elements - You prefer traditional namespace style - Backward compatibility with v0.7.x output
Custom Prefix Override
order.to_xml(prefix: "custom")Produces custom prefix (overrides prefix_default):
<custom:PurchaseOrder xmlns:custom="http://example.com/po">
<custom:orderDate>2024-01-15</custom:orderDate>
</custom:PurchaseOrder>Use this when: - Integrating with systems that expect specific prefixes - Avoiding prefix conflicts with other namespaces
Explicitly Disable Prefix
order.to_xml(prefix: false)Produces default namespace format and disables format preservation:
<PurchaseOrder xmlns="http://example.com/po">
<orderDate>2024-01-15</orderDate>
</PurchaseOrder>Use this when: - You want to override format preservation from parsed XML - You explicitly want default format regardless of input
Override Prefix for Specific Namespaces
order.to_xml(namespaces: [
{ namespace: PoNamespace, prefix: "purchase" },
{ namespace: DcNamespace, prefix: "dublin" }
])Produces custom prefixes per namespace:
<purchase:PurchaseOrder xmlns:purchase="http://example.com/po"
xmlns:dublin="http://purl.org/dc/elements/">
<purchase:orderDate>2024-01-15</purchase:orderDate>
<dublin:title>Order #123</dublin:title>
</purchase:PurchaseOrder>Use this for: - Fine-grained control over multiple namespaces - Matching specific schema requirements
Format Decision Priority
When you call to_xml, the format (default vs prefix) is decided using this priority order:
| Priority | Rule |
|---|---|
1 (Highest) | Stored plan from parsed XML - Round-trip preservation |
2 | Explicit user option - |
3 | W3C rules - Attributes MUST use prefix if in their own namespace |
4 | Qualified children - If children need prefix, parent provides it |
5 (Lowest) | Default preference - Prefer default namespace (cleaner) |
Understanding DeclarationPlan
The DeclarationPlan is the brain of XML namespace output. Before any XML is rendered, the plan decides:
-
Where each namespace is declared (root or local element)
-
What format each element uses (default or prefixed)
-
Which prefix to use for each namespace
How DeclarationPlan Works
The serialization uses a two-track system that produces parallel outputs:
Your Model Instance
│
├────────────────────────────────────────────────────────┐
│ │
▼ ▼
┌─────────────────────────┐ ┌─────────────────────────────┐
│ DATA MODEL TRACK │ │ DECLARATION PLAN TRACK │
│ (Content Layer) │ │ (Format Layer) │
│ │ │ │
│ XmlElement tree: │ │ DeclarationPlanner: │
│ - Element names │ │ - Format decisions │
│ - Structure │ │ - Prefix assignments │
│ - Namespace identity │ │ - Hoisting locations │
│ - Values │ │ │
│ - NO format info │ │ (from input or new) │
└─────────────────────────┘ └─────────────────────────────┘
│ │
│ │
└──────────────────────┬──────────────────────────────┘
│
▼
┌────────────────────────────────────┐
│ ADAPTER MERGE │
│ Parallel traversal: │
│ - Read XmlElement for content │
│ - Read DeclarationPlan for format │
│ - Generate XML with both │
└────────────────────────────────────┘
│
▼
Final XML OutputKey Insight:
-
DataModel (XmlElement): Contains WHAT to serialize (elements, values, structure)
-
DeclarationPlan: Contains HOW to serialize (format, prefixes, declarations)
-
Adapter: Merges both tracks during parallel traversal
Round-Trip Preservation:
When parsing XML, the input DeclarationPlan is stored:
# Parse creates both DataModel AND DeclarationPlan
doc = Order.from_xml(input_xml)
# doc stores:
# - XmlElement tree (content)
# - DeclarationPlan from input (format preserved)
# Serialize reuses stored plan
doc.to_xml # Format preserved from input!Common Scenarios and Solutions
"My XML looks different after upgrading!"
Problem: XML output changed from prefixed to default namespace format.
Solution: This is expected. v0.8.0 defaults to cleaner default namespace format.
To restore old format:
instance.to_xml(prefix: true)"Consuming system rejects my XML!"
Problem: External system requires specific namespace format.
Solutions:
-
If system requires prefixes:
instance.to_xml(prefix: true)
-
If system requires specific prefix:
instance.to_xml(prefix: "required_prefix")
-
If system requires multiple specific prefixes:
instance.to_xml(namespaces: [
{ namespace: Ns1, prefix: "a" },
{ namespace: Ns2, prefix: "b" }
])"My attributes need prefixes!"
Problem: Attributes should be prefixed but aren’t.
Solution: Set attribute_form_default :qualified in your namespace class:
class MyNamespace < Lutaml::Xml::Namespace
uri "http://example.com/ns"
prefix_default "my"
attribute_form_default :qualified # ✅ Force prefixed attributes
end"Prefix disappeared on child elements!"
Problem: Parent uses prefix, children don’t.
Solution: Set element_form_default :qualified:
class MyNamespace < Lutaml::Xml::Namespace
uri "http://example.com/ns"
prefix_default "my"
element_form_default :qualified # ✅ Children inherit namespace
endOr use to_xml(prefix: true) to force prefix format globally.
"I want round-trip preservation!"
Problem: Want parsed XML format preserved on output.
Solution: This happens automatically! The DeclarationPlan stores the input format and preserves it.
# Parse XML with prefixed format
parsed = Order.from_xml(prefixed_xml)
# Serialize - format is preserved!
parsed.to_xml # Still uses prefixed formatTo override preservation:
parsed.to_xml(prefix: false) # Force default format
parsed.to_xml(prefix: true) # Force prefix formatNamespace Hoisting with namespace_scope
The namespace_scope directive controls where namespace declarations appear in your XML output. This is called "Namespace Hoisting" - moving declarations from local elements to a higher (ancestor) element.
What is Namespace Hoisting?
By default, namespaces are declared on the first element that uses them (local declaration). Hoisting moves declarations to a designated ancestor element, typically the root.
| Local Declaration (Default) | Hoisted Declaration |
|---|---|
[source,xml] ---- <root> <parent> <child xmlns:dc="…"> <dc:title>…</dc:title> </child> </parent> </root> ---- | [source,xml] ---- <root xmlns:dc="…"> <parent> <child> <dc:title>…</dc:title> </child> </parent> </root> ---- |
Using namespace_scope
class Document < Lutaml::Model::Serializable
attribute :title, :string
attribute :creator, :string
xml do
element "document"
namespace DocNamespace
# Hoist these namespaces to this element
namespace_scope [
DcNamespace, # Auto-declare if used
DctermsNamespace, # Auto-declare if used
]
end
endDeclaration Modes
The declare: option controls when hoisted namespaces appear:
:auto (Default) - Declare Only If Used
namespace_scope [DcNamespace, DctermsNamespace]
# or explicitly:
namespace_scope [{ namespace: DcNamespace, declare: :auto }]Namespace declared at this element ONLY IF any descendant uses it.
Output when used:
<document xmlns="http://doc.example.com"
xmlns:dc="http://purl.org/dc/">
<dc:title>My Document</dc:title>
</document>Output when NOT used:
<document xmlns="http://doc.example.com">
<title>My Document</title>
</document> :always - Force Declaration
namespace_scope [{ namespace: VtNamespace, declare: :always }]Namespace declared at this element even if no descendant uses it.
Use cases: - Office Open XML requires xmlns:vt always present - Schema validation requires namespace declaration - Pre-declaring for dynamic content
Output:
<Properties xmlns="http://app.example.com"
xmlns:vt="http://vt.example.com">
<!-- vt namespace declared but NOT used -->
<Template>Normal.dotm</Template>
</Properties>Hoisting Use Cases
Space Efficiency
For documents with many repeated namespace usages, hoisting reduces file size:
Without hoisting (repeated declarations):
<root>
<item xmlns:dc="http://purl.org/dc/">
<dc:title>Title 1</dc:title>
</item>
<item xmlns:dc="http://purl.org/dc/">
<dc:title>Title 2</dc:title>
</item>
<item xmlns:dc="http://purl.org/dc/">
<dc:title>Title 3</dc:title>
</item>
</root>With hoisting (single declaration):
<root xmlns:dc="http://purl.org/dc/">
<item>
<dc:title>Title 1</dc:title>
</item>
<item>
<dc:title>Title 2</dc:title>
</item>
<item>
<dc:title>Title 3</dc:title>
</item>
</root>Round-Trip Preservation
When parsing XML, the input’s namespace declaration structure is preserved:
# Input has hoisted namespaces
input_xml = <<~XML
<document xmlns:dc="http://purl.org/dc/">
<dc:title>Example</dc:title>
</document>
XML
doc = Document.from_xml(input_xml)
# Output preserves hoisting
doc.to_xml # Still has xmlns:dc on rootCompatibility with Non-Namespace-Aware Software
Some legacy XML parsers don’t properly handle local namespace declarations. Hoisting all namespaces to root ensures compatibility:
class LegacyFormat < Lutaml::Model::Serializable
xml do
element "data"
namespace MainNamespace
# Hoist ALL namespaces to root for legacy compatibility
namespace_scope [
{ namespace: Ns1, declare: :always },
{ namespace: Ns2, declare: :always },
{ namespace: Ns3, declare: :always },
]
end
endThis ensures all namespaces are declared at root, even if unused:
<data xmlns="http://main"
xmlns:ns1="http://ns1"
xmlns:ns2="http://ns2"
xmlns:ns3="http://ns3">
<item>value</item>
</data>Multiple namespace_scope Directives
If parent and child both use namespace_scope, the child’s scope is additive:
class Parent < Lutaml::Model::Serializable
xml do
element "parent"
namespace_scope [Ns1, Ns2] # Parent hoists Ns1, Ns2
end
end
class Child < Lutaml::Model::Serializable
xml do
element "child"
namespace_scope [Ns3] # Child additionally hoists Ns3
end
endAttribute Name Conflicts
Version 0.8.0 simplifies attribute name conflict handling:
-
No more errors for attribute names that conflict with built-in methods
-
Only warnings are shown for potentially problematic overrides
-
All attribute names are now usable
# Before (v0.7.x) - Error raised
attribute :display, :string # ❌ InvalidAttributeNameError
# After (v0.8.0) - Warning only
attribute :display, :string # ✅ Works (shows warning)no_root Method Deprecated
The no_root method is deprecated. Simply omit the element() declaration for type-only models.
Testing Your Migration
After updating, run your test suite:
bundle exec rake specPay attention to:
-
Deprecation warnings in the output
-
XML format changes (prefixes vs default namespaces)
-
Attribute prefixing behavior
Troubleshooting
Problem: XML looks different after upgrading
Solution: This is expected. The new default uses clean XML with default namespaces. To restore old format:
instance.to_xml(prefix: true)Problem: Consuming system rejects my XML
Solution 1: Check if system supports default namespaces (most do).
Solution 2: If system requires prefixes:
instance.to_xml(prefix: true)Problem: tomlib fails on Windows
Solution: Use :toml_rb adapter:
config.toml_adapter_type = :toml_rbSummary Checklist
Configuration & Adapters
-
Update adapter configuration to use new simplified names (
:standardinstead of:standard_json) -
Update explicit adapter class references to new paths
-
Use
:toml_rbon Windows instead of:tomlib
XML Namespace Changes
-
Review XML output format changes (prefix vs default namespace)
-
Set
attribute_form_default :qualifiedif you need prefixed attributes -
Remove
prefixparameters from mapping methods -
Create child model classes for elements with different namespaces
-
Use
namespace_scopefor consolidated namespace declarations
Model Definition Changes
-
Replace
root "name"withelement "name"for standalone models -
Remove
no_rootcalls - models withoutelement()are type-only by default -
Use
type_name "TypeName"for XSD type naming -
Move namespace definitions from mappings to model classes
-
Update Type classes to use unified
xml dosyntax
New XML Architecture Concepts
Version 0.8.0 introduces a completely revamped XML serialization architecture. Understanding these concepts is essential for working with complex XML documents.
Three-Phase Architecture
The XML serialization now uses a sophisticated three-phase architecture that is W3C-compliant while handling complex model hierarchies:
Phase 1: Discovery
Phase 1A: Identity Discovery (Bottom-Up)
Walks the model tree from leaves to root, collecting all namespace identities of models and value types.
Phase 1B: Scope Coverage Discovery (Top-Down)
Determines the hoisting eligibility of each namespace based on namespace_scope directives and namespace identity.
DeclarationPlan
The DeclarationPlan is a new central concept that stores ALL namespace decisions before rendering:
# The plan contains:
DeclarationPlan = {
# Namespace declarations
namespace_declarations: {
"po" => {
uri: "http://example.com/po",
prefix: "po",
format: :default, # or :prefix
}
},
# Element format decisions
element_formats: {
"purchaseOrder" => :default,
"title" => :prefix, # Type namespace
},
# Prefix assignments
prefix_assignments: {
PoNamespace => "po",
DcNamespace => "dc",
}
}Key Principle: Adapters are "dumb" - they ONLY apply decisions from the DeclarationPlan. They NEVER make namespace decisions themselves.
XmlDataModel (Content Layer)
The XmlDataModel is an intermediate representation between your Model and XML:
-
Content Layer: Defines WHAT to serialize (element names, structure, namespace identity)
-
Namespace Identity: Knows which namespace each element belongs to
-
No Format Info: Does NOT specify prefix vs default (DeclarationPlan’s job)
Namespace Scopes
The namespace_scope directive controls WHERE namespace declarations appear:
xml do
root "vCard"
namespace VcardNamespace
# Consolidate namespaces at root
namespace_scope [
{ namespace: DcNamespace, declare: :always },
{ namespace: DctermsNamespace, declare: :auto }
]
endDeclaration modes:
-
:auto- Declare if any descendant element/attribute belongs to this namespace -
:always- Always declare at this element (unless ancestor already declared) -
:never- Never declare at this element
Decision System (Element Format)
The Decision System determines whether each element uses default or prefixed format. Rules are evaluated in priority order:
-
Priority 0: Inherit parent’s default namespace
-
Priority 0.5: Use prefix hoisted on parent
-
Priority 1: User explicit option (
to_xml(prefix: true/false/"custom")) -
Priority 2: Preserved format from input (round-trip)
-
Priority 3: W3C disambiguation (attributes need prefix)
-
Priority 4: namespace_scope (multiple namespaces at root)
-
Priority 5: Prefer default format (cleanest)
TypeNamespace Subsystem
Type namespaces (declared on custom Type classes) are now handled by a dedicated subsystem:
# Define type with namespace
class DcTitleType < Lutaml::Model::Type::String
xml_namespace DublinCoreNamespace # Type-level namespace
end
# Use in model
class Document < Lutaml::Model::Serializable
attribute :title, DcTitleType # Type namespace applies automatically
xml do
root "document"
map_element "title", to: :title
end
endKey rules:
-
Type namespace as attribute → MUST use prefix (W3C constraint)
-
Type namespace as element with same namespace as parent → Can inherit
-
Type namespace as element with different namespace → Can use default OR prefix
XmlNamespace Class
The XmlNamespace class encapsulates all namespace configuration:
class PoNamespace < Lutaml::Xml::Namespace
uri "http://example.com/po" # Required: namespace URI
prefix_default "po" # Required: default prefix
element_form_default :qualified # Optional: children inherit namespace
attribute_form_default :unqualified # Optional: attributes unprefixed
endW3C Compliance:
-
element_form_default :unqualified(default) - Children in blank namespace -
element_form_default :qualified- Children inherit parent namespace -
attribute_form_default :unqualified(default) - Attributes have NO namespace -
attribute_form_default :qualified- Attributes MUST use prefix format
Benefits of New Architecture
-
Single Source of Truth: DeclarationPlanner makes ALL xmlns decisions
-
Full Tree Knowledge: NamespaceCollector provides complete context before decisions
-
Never Declare Twice: xmlns declared at optimal location, children reference it
-
Clean Separation: Three independent, testable phases
-
Circular Reference Handling: Built-in recursion prevention
-
Type-Only Model Support: Models without element wrapper fully supported
-
Round-Trip Preservation: Input format preserved during serialization
Key Design Patterns
Dumb Adapter Pattern
Adapters ONLY apply decisions from DeclarationPlan - they NEVER make namespace decisions:
Planning Phase Rendering Phase
┌─────────────────────┐ ┌─────────────────────┐
│ DeclarationPlanner │ │ NokogiriAdapter │
│ - MAKE DECISIONS │ ───▶ │ - Read plan │
│ - Create plan │ │ - Apply xmlns │
└─────────────────────┘ │ - NO DECISIONS │
└─────────────────────┘Understanding xmlns="" Behavior
When REQUIRED:
-
Child in different namespace than parent
-
Parent uses default namespace format (
xmlns="uri") -
Child’s namespace has no prefix on child element
<!-- CORRECT -->
<parent xmlns="http://example.com/parent">
<child xmlns="">Value</child>
<!-- xmlns="" REQUIRED to opt out -->
</parent>
<!-- CORRECT: Child has prefix -->
<parent xmlns="http://example.com/parent">
<other:child xmlns:other="http://other">Value</other:child>
<!-- No xmlns="" needed - prefix makes it clear -->
</parent>DataModel Transformation Architecture
Version 0.8.0 introduces a two-phase transformation pipeline for all serialization formats. Instead of directly serializing Lutaml::Model instances, the system now uses an intermediate representation.
Transformation Pipeline
┌─────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ Lutaml::Model │ │ Format DataModel │ │ Serialization Format│
│ Instance │ ──▶ │ (Intermediate) │ ──▶ │ (String/Bytes) │
└─────────────────┘ └─────────────────────┘ └─────────────────────┘
│ │ │
Your model XmlElement, XML, JSON, YAML,
classes JsonObject, TOML, Hash
YamlNode, etc.Phase 1: Model to DataModel Transformation
Each format has a dedicated Transformation class that converts your model instances into format-specific DataModel objects:
# XML Transformation
transformation = Lutaml::Xml::Transformation.new
xml_data_model = transformation.apply(person_instance)
# => XmlElement tree with children, attributes, namespaces
# JSON Transformation
transformation = Lutaml::Json::Transformation.new
json_data_model = transformation.apply(person_instance)
# => JsonObject treePhase 2: DataModel to Serialization Format
The DataModel objects are then serialized by the adapter:
# XML adapter serializes XmlElement tree
xml_string = Lutaml::Xml::Adapter.serialize(xml_data_model)
# JSON adapter serializes JsonObject tree
json_string = Lutaml::Json::Adapter.serialize(json_data_model)Benefits
-
Format-agnostic logic: Business logic stays in your models
-
Testable intermediate representation: Debug DataModel objects directly
-
Clean separation: Transformation logic isolated from serialization
-
Extensibility: Add new formats by creating new Transformation classes
-
Round-trip support: Parse → DataModel → Model → DataModel → Serialize
Consolidated xml do Syntax
Version 0.8.0 unifies the xml do syntax for both model classes and custom Type classes.
Before (v0.7.x) - Different Syntaxes
# Model class - used namespace method
class Person < Lutaml::Model::Serializable
xml do
namespace "http://example.com/ns", "p" # Different syntax
root "person"
end
end
# Type class - used xml_namespace method
class CustomType < Lutaml::Model::Type::String
xml_namespace CustomNamespace # Different method name!
endAfter (v0.8.0) - Unified Syntax
Both models and types now use the same xml do block syntax:
# Model class
class Person < Lutaml::Model::Serializable
xml do
namespace PersonNamespace # Namespace class reference
element "person" # element() replaces root()
end
end
# Type class - SAME syntax!
class CustomType < Lutaml::Model::Type::String
xml do
namespace CustomNamespace # Same method
# No element() - types don't have root elements
end
endXSD-Aligned Concepts: Element vs ComplexType
Version 0.8.0 aligns Lutaml::Model concepts with W3C XML Schema (XSD) terminology for better clarity and standards compliance.
Element Declaration
An Element is a model that can be serialized as a standalone XML document with a root element.
class PurchaseOrder < Lutaml::Model::Serializable
attribute :order_date, :date
attribute :items, Item, collection: true
xml do
element "purchaseOrder" # ✅ Element - can be root
namespace PoNamespace
map_element "orderDate", to: :order_date
map_element "item", to: :items
end
end
# Can be serialized standalone
order = PurchaseOrder.new(...)
order.to_xml # => <purchaseOrder>...</purchaseOrder>ComplexType (Type-Only Model)
A ComplexType is a model that defines a type structure but cannot be serialized as a standalone document. It must be embedded in another model.
# No element() declaration = ComplexType (type-only)
class Address < Lutaml::Model::Serializable
attribute :street, :string
attribute :city, :string
xml do
# No element() - this is a ComplexType
type_name "AddressType" # Optional: XSD type name
map_element "street", to: :street
map_element "city", to: :city
end
end
# Cannot be serialized standalone
address = Address.new(...)
address.to_xml # => NoRootMappingError
# Must be used in an Element
class Person < Lutaml::Model::Serializable
attribute :address, Address # Embed ComplexType
xml do
element "person" # Element
map_element "address", to: :address
end
endModel-Centric Namespace Definitions
Version 0.8.0 enforces model-centric namespace definitions. Namespaces are defined ONLY on models and types, not on individual mappings.
Before (v0.7.x) - Namespace on Mappings
class Parent < Lutaml::Model::Serializable
xml do
root "parent"
namespace "http://parent.com", "p"
# ❌ Namespace on mapping - NO LONGER SUPPORTED
map_element "child", to: :child, namespace: ChildNamespace
map_element "other", to: :other, prefix: "o"
end
endAfter (v0.8.0) - Namespace on Models Only
# Child model has its own namespace
class Child < Lutaml::Model::Serializable
attribute :value, :string
xml do
element "child"
namespace ChildNamespace # ✅ Namespace on model
map_content to: :value
end
end
# Parent references child model
class Parent < Lutaml::Model::Serializable
attribute :child, Child # Typed attribute
attribute :other, Other
xml do
element "parent"
namespace ParentNamespace # ✅ Namespace on model
map_element "child", to: :child # No namespace parameter
map_element "other", to: :other
end
endWhy This Change
-
Single source of truth: Each model owns its namespace
-
Reusability: Child models work in any parent context
-
Clarity: No confusion about which namespace applies
-
XSD alignment: Matches XSD’s type-centric namespace model
Migration
# Before: Inline namespace on mapping
map_element "item", to: :item, namespace: ItemNamespace
# After: Create separate model with namespace
class Item < Lutaml::Model::Serializable
attribute :value, :string
xml do
element "item"
namespace ItemNamespace
map_content to: :value
end
end
# Then use typed attribute
attribute :item, Item
map_element "item", to: :itemRegister System Refactoring
Version 0.8.0 significantly enhances the Register system with hierarchical fallbacks, composition, and key-based registration.
Hierarchical Fallback Chains
Registers can now delegate to parent registers when a type is not found:
# Parent register with base types
base_register = Lutaml::Model::Register.new do
register_type(:string, Lutaml::Model::Type::String)
register_type(:integer, Lutaml::Model::Type::Integer)
end
# Child register with domain types, falls back to parent
domain_register = Lutaml::Model::Register.new(
fallback: base_register # ✅ Hierarchical fallback
) do
register_type(:custom_id, CustomIdType)
end
# Resolution tries child first, then parent
domain_register.resolve_type(:string) # => From base_register
domain_register.resolve_type(:custom_id) # => From domain_registerRegister Composition
Multiple registers can be composed together:
# Create specialized registers
core_types = Lutaml::Model::Register.new { ... }
xml_types = Lutaml::Model::Register.new { ... }
domain_types = Lutaml::Model::Register.new { ... }
# Compose into unified register
composed = Lutaml::Model::Register.composite(
[domain_types, xml_types, core_types]
)Key-Based Registration
Types can be registered under specific keys for namespacing:
register = Lutaml::Model::Register.new do
# Register with key for namespacing
register_type(:id, MyIdType, key: :myapp)
register_type(:name, MyNameType, key: :myapp)
end
# Resolve with key
register.resolve_type(:id, key: :myapp) # => MyIdType
register.resolve_type(:id) # => nil (key required)XSD Schema Generation and Parsing
Version 0.8.0 integrates XSD schema capabilities from lutaml-xsd, enabling both generation and parsing of XML Schema documents.
Generating XSD from Models
class Person < Lutaml::Model::Serializable
attribute :name, :string
attribute :age, :integer
attribute :email, :string
xml do
element "person"
map_element "name", to: :name
map_element "age", to: :age
map_attribute "email", to: :email
end
end
# Generate XSD schema
schema = Lutaml::Model::Schema::XmlCompiler.to_xsd(Person)
puts schema
# => <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
# <xs:element name="person">
# <xs:complexType>
# <xs:sequence>
# <xs:element name="name" type="xs:string"/>
# <xs:element name="age" type="xs:integer"/>
# </xs:sequence>
# <xs:attribute name="email" type="xs:string"/>
# </xs:complexType>
# </xs:element>
# </xs:schema>Parsing XSD to Models
# Parse XSD file into Ruby model classes
models = Lutaml::Model::Schema::XmlCompiler.from_xsd("schema.xsd")
# Returns array of generated model classes
models.each do |model_class|
puts model_class.name # => "PurchaseOrder", "Address", etc.
end
# Use generated models
order_data = models.find { |m| m.name == "PurchaseOrder" }
order = order_data.from_xml(xml_string)