Overview
Version 0.8.0 integrates the XSD schema capabilities from lutaml-xsd directly into lutaml-model, providing comprehensive XML Schema (XSD) support:
-
XSD Generation - Convert Ruby models to XML Schema documents
-
XSD Parsing - Parse XSD files into Ruby model objects
-
Code Generation - Compile XSD into Ruby model class files
-
Custom XSD Types - Define XSD types on
Type::Valuesubclasses -
RelaxNG Generation - Generate RELAX NG schemas from models
-
XSD Validation - Pre-parsing validation of XSD documents
This integration means lutaml-xsd is no longer a separate dependency - all XSD functionality is now built into lutaml-model.
Quick Start
Generate XSD from Ruby Models
require 'lutaml/model'
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
xsd = Lutaml::Model::Schema.to_xsd(Person)
puts xsdOutput:
<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>Parse XSD into Ruby Model Classes
# Parse XSD and generate Ruby files
Lutaml::Model::Schema.from_xml(xsd_content,
output_dir: "lib/models",
create_files: true,
module_namespace: "MyApp"
)
# Or load classes directly into memory
classes = Lutaml::Model::Schema.from_xml(xsd_content, load_classes: true)XSD Generation
Three Generation Patterns
The XSD generator supports three patterns depending on your model configuration:
Pattern 1: Anonymous Inline ComplexType
When only an element name is declared (no type_name), generates an inline <complexType> inside the <element>.
class Person < Lutaml::Model::Serializable
attribute :name, :string
xml do
element "person" # Only element name
map_element "name", to: :name
end
end<xs:element name="person">
<xs:complexType>
<xs:sequence>
<xs:element name="name" type="xs:string"/>
</xs:sequence>
</xs:complexType>
</xs:element>Pattern 2: Named Reusable ComplexType
When only type_name is declared (no element), generates a standalone <complexType> without an <element> declaration.
class Address < Lutaml::Model::Serializable
attribute :street, :string
attribute :city, :string
xml do
type_name "AddressType" # Only type name, no element
map_element "street", to: :street
map_element "city", to: :city
end
end<xs:complexType name="AddressType">
<xs:sequence>
<xs:element name="street" type="xs:string"/>
<xs:element name="city" type="xs:string"/>
</xs:sequence>
</xs:complexType>Pattern 3: Element with Named Type
When both element name and type_name are declared, generates an <element> with a type attribute plus a separate <complexType>.
class Person < Lutaml::Model::Serializable
attribute :name, :string
xml do
element "person"
type_name "PersonType" # Both element and type
map_element "name", to: :name
end
end<xs:element name="person" type="PersonType"/>
<xs:complexType name="PersonType">
<xs:sequence>
<xs:element name="name" type="xs:string"/>
</xs:sequence>
</xs:complexType>Namespace Support
XSD generation respects XmlNamespace declarations:
class PoNamespace < Lutaml::Model::XmlNamespace
uri "http://example.com/po"
prefix_default "po"
element_form_default :qualified
end
class PurchaseOrder < Lutaml::Model::Serializable
attribute :order_date, :date
xml do
element "purchaseOrder"
namespace PoNamespace
map_attribute "orderDate", to: :order_date
end
end
xsd = Lutaml::Model::Schema.to_xsd(PurchaseOrder)Output:
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:po="http://example.com/po"
targetNamespace="http://example.com/po">
<xs:element name="purchaseOrder">
<xs:complexType>
<xs:attribute name="orderDate" type="xs:date"/>
</xs:complexType>
</xs:element>
</xs:schema>Collection Support
Collections generate minOccurs and maxOccurs attributes:
class Order < Lutaml::Model::Serializable
attribute :items, :string, collection: true
xml do
element "order"
map_element "item", to: :items
end
end<xs:element name="item" type="xs:string" minOccurs="0" maxOccurs="unbounded"/>Nested Models
Nested models are handled automatically:
class Address < Lutaml::Model::Serializable
attribute :city, :string
xml do
element "address"
map_element "city", to: :city
end
end
class Person < Lutaml::Model::Serializable
attribute :address, Address
xml do
element "person"
map_element "address", to: :address
end
endCustom XSD Types
The xsd_type Directive
Custom Type::Value subclasses can define their XSD type using the xsd_type directive:
class IdType < Lutaml::Model::Type::String
xsd_type "xs:ID"
end
class TokenType < Lutaml::Model::Type::String
xsd_type "xs:token"
end
class LanguageType < Lutaml::Model::Type::String
xsd_type "xs:language"
endWhen used in models, the custom XSD type is used in generated schemas:
class Product < Lutaml::Model::Serializable
attribute :id, IdType
attribute :code, TokenType
xml do
element "product"
map_attribute "id", to: :id
map_element "code", to: :code
end
end<xs:attribute name="id" type="xs:ID"/>
<xs:element name="code" type="xs:token"/>Type Inheritance
The xsd_type is inherited by subclasses:
class BaseType < Lutaml::Model::Type::String
xsd_type "xs:token"
end
class DerivedType < BaseType
# Inherits xsd_type "xs:token"
endSupported XSD Types
The default type mapping is:
| Lutaml::Model Type | XSD Type |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
XSD Parsing and Code Generation
Basic Parsing
Parse an XSD file into Ruby model objects:
xsd_content = <<~XML
<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:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
XML
# Parse into model objects
schema = Lutaml::Xml::Schema::Xsd.parse(xsd_content)Code Generation Options
Generate Ruby files from XSD:
Lutaml::Model::Schema.from_xml(xsd_content,
output_dir: "lib/my_app/models", # Directory for generated files
create_files: true, # Write files to disk
load_classes: false, # Don't load into memory
module_namespace: "MyApp", # Wrap in module
register_id: :my_app, # Register for type resolution
location: "schemas/", # Base location for imports
namespace: "http://example.com", # Target namespace
indent: 2 # Indentation level
)Generated Code Structure
For a complex XSD, the generator creates:
-
Model classes -
Lutaml::Model::Serializablesubclasses with attributes and mappings -
Type classes -
Lutaml::Model::Type::Valuesubclasses for restricted simple types -
Namespace classes -
Lutaml::Model::XmlNamespacesubclasses -
Registry file - Central registry with autoload and
register_allmethod
Handling Imports and Includes
The parser handles <xs:import> and <xs:include> recursively:
Lutaml::Model::Schema.from_xml(xsd_content,
location: "https://example.com/schemas/",
schema_mappings: {
"http://example.com/types" => "vendor/types.xsd"
}
)Generated Type Validations
Simple type restrictions generate validation code:
# From XSD:
# <xs:simpleType name="PositiveInteger">
# <xs:restriction base="xs:integer">
# <xs:minInclusive value="1"/>
# </xs:restriction>
# </xs:simpleType>
# Generates:
class PositiveInteger < Lutaml::Model::Type::Integer
def cast(value)
result = super
raise ValidationError, "must be >= 1" if result < 1
result
end
endSupported restrictions: * minInclusive, maxInclusive * minExclusive, maxExclusive * length, minLength, maxLength * pattern * enumeration * whiteSpace * totalDigits, fractionDigits
XSD Model Classes
The XSD parser uses model classes for each XSD element type:
| XSD Element | Ruby Class |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
You can parse XSD directly into these model objects:
require 'lutaml/xml/schema/xsd'
schema = Lutaml::Xml::Schema::Xsd::Schema.from_xml(xsd_string)
schema.elements.each do |element|
puts "Element: #{element.name}"
puts "Type: #{element.type}" if element.type
endXSD Validation
Pre-parsing validation checks XSD syntax and structure:
# Validation is automatic during parsing
schema = Lutaml::Xml::Schema::Xsd.parse(xsd_content, validate_schema: true)
# Or validate explicitly
validator = Lutaml::Xml::Schema::Xsd::SchemaValidator.new(xsd_content)
validator.validate # raises ValidationError if invalidValidation includes: * XML syntax validation * Proper <schema> root element * XSD version detection (1.0 vs 1.1) * XSD 1.1-specific element detection
RelaxNG Generation
Generate RELAX NG schemas from Ruby models:
class Person < Lutaml::Model::Serializable
attribute :name, :string
attribute :age, :integer
xml do
element "person"
map_element "name", to: :name
map_element "age", to: :age
end
end
rng = Lutaml::Model::Schema.to_relaxng(Person)Output:
<grammar xmlns="http://relaxng.org/ns/structure/1.0">
<start>
<ref name="person"/>
</start>
<define name="person">
<element name="person">
<element name="name">
<text/>
</element>
<element name="age">
<text/>
</element>
</element>
</define>
</grammar>attribute_form_default :qualified Fix
Version 0.8.0 also fixes a bug where attribute_form_default :qualified in XmlNamespace classes was not being respected during XML serialization.
What Was Fixed
Prior to v0.8.0, when a namespace declared attribute_form_default :qualified, attributes were incorrectly serialized without namespace prefixes.
class MyNamespace < Lutaml::Model::XmlNamespace
uri "http://example.com/ns"
prefix_default "ex"
attribute_form_default :qualified # Now WORKS correctly
end
class Item < Lutaml::Model::Serializable
attribute :id, :string
attribute :value, :integer
xml do
element "item"
namespace MyNamespace
map_attribute "id", to: :id
map_attribute "value", to: :value
end
end
item = Item.new(id: "123", value: 42)
item.to_xml(prefix: true)Correct Output (v0.8.0+):
<ex:item xmlns:ex="http://example.com/ns" ex:id="123" ex:value="42"/>Migration from v0.7.x
Summary of Changes
| Aspect | v0.7.x | v0.8.0 |
|---|---|---|
XSD support | Separate lutaml-xsd gem | Integrated into lutaml-model |
XSD generation | Limited | Full support with patterns |
XSD parsing | Manual | Automatic code generation |
Custom types | Not supported |
|
RelaxNG | Not supported | Supported |
| Buggy | Fixed |
Migration Steps
-
Remove lutaml-xsd dependency - No longer needed
# Gemfile - REMOVE this line
gem 'lutaml-xsd'
# Keep only
gem 'lutaml-model', '~> 0.8.0'-
Update XSD generation code
# OLD (v0.7.x)
require 'lutaml/xsd'
schema = Lutaml::Xsd::Schema.from_xml(xsd)
# NEW (v0.8.0)
require 'lutaml/model'
schema = Lutaml::Xml::Schema::Xsd::Schema.from_xml(xsd)
# Or use the public API
Lutaml::Model::Schema.from_xml(xsd)-
Update custom type definitions
# OLD - No way to specify XSD type
# NEW - Use xsd_type directive
class IdType < Lutaml::Model::Type::String
xsd_type "xs:ID"
end-
Verify
attribute_form_defaultbehavior
If you use attribute_form_default :qualified, verify your XML output has properly prefixed attributes. This was a bug fix, so your output may change.
Troubleshooting
XSD Generation Issues
Problem: Missing type references in generated XSD
Solution: Ensure all referenced model classes are loaded before generating:
# Load all models first
require_relative 'models/address'
require_relative 'models/person'
# Then generate
Lutaml::Model::Schema.to_xsd(Person)Problem: Custom XSD types not appearing
Solution: Define xsd_type on your Type class:
class MyCustomType < Lutaml::Model::Type::String
xsd_type "xs:token" # Required for custom types
endCode Generation Issues
Problem: Import/include not resolved
Solution: Provide location and schema_mappings:
Lutaml::Model::Schema.from_xml(xsd,
location: "schemas/",
schema_mappings: {
"http://external.com/types" => "vendor/types.xsd"
}
)Problem: Generated classes not loading
Solution: Ensure output directory is in load path, or use load_classes: true:
# Option 1: Load during generation
classes = Lutaml::Model::Schema.from_xml(xsd, load_classes: true)
# Option 2: Add to load path
$LOAD_PATH.unshift("lib/my_app/models")
require 'my_app/registry'
MyApp.register_all