- Introduction
- Quick Start
- XSD Architecture Overview
- SimpleTypes: Value Type Definition
- ComplexTypes: Three Patterns
- Nested ComplexTypes
- Attributes vs Elements
- Collections and Cardinality
- Namespaces in XSD
- Complete Example: E-Commerce Catalog
- Schema Generation Options
- Best Practices
- Common Patterns
- Troubleshooting
- Type Resolution and Validation
- See Also
Introduction
This guide teaches you how to generate W3C XML Schema (XSD) definitions from LutaML models, covering the complete mapping between LutaML constructs and XSD declarations.
Quick Start
# Generate XSD from a model
xsd_string = Lutaml::Model::Schema.to_xsd(MyModel)
# Save to file
File.write("schema.xsd", xsd_string)
# With options
xsd = Lutaml::Model::Schema.to_xsd(MyModel,
adapter: :nokogiri,
encoding: "UTF-8",
pretty: true
)XSD Architecture Overview
Two Type Systems
W3C XSD has two fundamental type systems:
| XSD Type | Purpose | LutaML Equivalent |
|---|---|---|
simpleType | Primitive values (strings, numbers, dates) |
|
complexType | Structured objects with attributes and elements |
|
LutaML to XSD Mapping Table
LutaML Construct → XSD Declaration
──────────────────────── ─────────────────
Type::Value.xsd_type "xs:ID" → <xs:simpleType> or built-in type
Serializable.element "x" → <xs:element name="x">
Serializable.type_name "XType" → <xs:complexType name="XType">
map_element "elem", to: :attr → <xs:element name="elem" type="...">
map_attribute "attr", to: :a → <xs:attribute name="attr" type="...">SimpleTypes: Value Type Definition
Built-in XSD Types
LutaML provides automatic XSD type mapping for built-in types:
class Product < Lutaml::Model::Serializable
attribute :name, :string # → xs:string
attribute :count, :integer # → xs:integer
attribute :price, :float # → xs:decimal
attribute :available, :boolean # → xs:boolean
attribute :created_at, :date # → xs:date
attribute :updated_at, :time # → xs:dateTime
endCustom SimpleTypes
For specialized XSD types (xs:ID, xs:IDREF, xs:token, etc.), create custom Value types:
class IdType < Lutaml::Model::Type::String
xsd_type 'xs:ID' # ← Declares XSD simpleType
def self.cast(value)
id = value.to_s.strip
unless id.match?(/\A[A-Za-z_][\w.\-]*\z/)
raise Lutaml::Model::TypeError, "Invalid XML ID: #{id}"
end
id
end
end
# Register for convenience
Lutaml::Model::Type.register(:id, IdType)
# Use in models
class Product < Lutaml::Model::Serializable
attribute :product_id, :id # Uses IdType → xs:ID in XSD
endGenerated XSD:
<xs:attribute name="productId" type="xs:ID"/>Common XSD SimpleTypes
# Identity types
class IdType < Lutaml::Model::Type::String
xsd_type 'xs:ID'
def self.cast(value)
id = value.to_s.strip
unless id.match?(/\A[A-Za-z_][\w.\-]*\z/)
raise Lutaml::Model::TypeError, "Invalid XML ID: #{id}"
end
id
end
end
class IdRefType < Lutaml::Model::Type::String
xsd_type 'xs:IDREF'
def self.cast(value)
id = value.to_s.strip
unless id.match?(/\A[A-Za-z_][\w.\-]*\z/)
raise Lutaml::Model::TypeError, "Invalid XML IDREF: #{id}"
end
id
end
end
class IdRefsType < Lutaml::Model::Type::String
xsd_type 'xs:IDREFS'
def self.cast(value)
case value
when String then value.split(/\s+/)
when Array then value
else [value.to_s]
end
end
end
# String variants
class TokenType < Lutaml::Model::Type::String
xsd_type 'xs:token'
def self.cast(value)
super(value).strip.gsub(/\s+/, ' ')
end
end
class LanguageType < Lutaml::Model::Type::String
xsd_type 'xs:language'
def self.cast(value)
lang = super(value).downcase
unless lang.match?(/\A[a-z]{2,3}(-[A-Za-z0-9]+)*\z/i)
raise Lutaml::Model::TypeError, "Invalid language code: #{lang}"
end
lang
end
end
class NormalizedStringType < Lutaml::Model::Type::String
xsd_type 'xs:normalizedString'
def self.cast(value)
super(value).gsub(/[\r\n\t]/, ' ')
end
end
# Numeric types with constraints
class PositiveIntegerType < Lutaml::Model::Type::Integer
xsd_type 'xs:positiveInteger'
def self.cast(value)
num = super(value)
raise Lutaml::Model::TypeError, "Must be positive" if num <= 0
num
end
end
class NonNegativeIntegerType < Lutaml::Model::Type::Integer
xsd_type 'xs:nonNegativeInteger'
def self.cast(value)
num = super(value)
raise Lutaml::Model::TypeError, "Cannot be negative" if num < 0
num
end
end
# Register all for convenience
Lutaml::Model::Type.register(:id, IdType)
Lutaml::Model::Type.register(:idref, IdRefType)
Lutaml::Model::Type.register(:idrefs, IdRefsType)
Lutaml::Model::Type.register(:token, TokenType)
Lutaml::Model::Type.register(:language, LanguageType)
Lutaml::Model::Type.register(:normalized_string, NormalizedStringType)
Lutaml::Model::Type.register(:positive_integer, PositiveIntegerType)
Lutaml::Model::Type.register(:non_negative_integer, NonNegativeIntegerType)ComplexTypes: Three Patterns
Pattern Overview
W3C XSD supports three patterns for complexTypes. LutaML differentiates them by whether element and type_name are called:
| Pattern | LutaML Declaration | When to Use |
|---|---|---|
Anonymous Inline |
| Single-use element structure |
Named Reusable |
| Type shared by multiple elements |
Element + Type |
| Element with explicitly named type |
Pattern 1: Anonymous Inline ComplexType
Use when: Element structure is unique and not reused.
class Product < Lutaml::Model::Serializable
attribute :name, :string
attribute :price, :float
xml do
element "product" # ← Only element declared
map_element "name", to: :name
map_element "price", to: :price
end
endGenerated XSD:
<xs:element name="product">
<xs:complexType>
<xs:sequence>
<xs:element name="name" type="xs:string"/>
<xs:element name="price" type="xs:decimal"/>
</xs:sequence>
</xs:complexType>
</xs:element>Pattern 2: Named ComplexType (Type-Only Model)
Use when: ComplexType should be reusable by multiple elements.
class ProductType < Lutaml::Model::Serializable
attribute :name, :string
attribute :price, :float
xml do
type_name "ProductType" # ← Only type declared, no element
map_element "name", to: :name
map_element "price", to: :price
end
endGenerated XSD:
<xs:complexType name="ProductType">
<xs:sequence>
<xs:element name="name" type="xs:string"/>
<xs:element name="price" type="xs:decimal"/>
</xs:sequence>
</xs:complexType>Usage: Other models can reference this type:
<xs:element name="product" type="ProductType"/>
<xs:element name="item" type="ProductType"/>Pattern 3: Element with Named ComplexType
Use when: Want both element declaration AND named type (best of both worlds).
class Product < Lutaml::Model::Serializable
attribute :name, :string
attribute :price, :float
xml do
element "product" # ← Element declared
type_name "ProductType" # ← Type named
map_element "name", to: :name
map_element "price", to: :price
end
endGenerated XSD:
<xs:element name="product" type="ProductType"/>
<xs:complexType name="ProductType">
<xs:sequence>
<xs:element name="name" type="xs:string"/>
<xs:element name="price" type="xs:decimal"/>
</xs:sequence>
</xs:complexType>Benefits:
-
Element declaration for direct use
-
Named type for references by other elements
-
Type can be extended or restricted
Nested ComplexTypes
Composition
Models can contain other models, creating nested complexTypes:
class Address < Lutaml::Model::Serializable
attribute :street, :string
attribute :city, :string
xml do
type_name "AddressType"
map_element "street", to: :street
map_element "city", to: :city
end
end
class Customer < Lutaml::Model::Serializable
attribute :name, :string
attribute :address, Address
xml do
element "customer"
type_name "CustomerType"
map_element "name", to: :name
map_element "address", to: :address
end
endGenerated XSD:
<xs:element name="customer" type="CustomerType"/>
<xs:complexType name="CustomerType">
<xs:sequence>
<xs:element name="name" type="xs:string"/>
<xs:element name="address" type="AddressType"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="AddressType">
<xs:sequence>
<xs:element name="street" type="xs:string"/>
<xs:element name="city" type="xs:string"/>
</xs:sequence>
</xs:complexType>Attributes vs Elements
XML Attributes
Declared with map_attribute:
class Product < Lutaml::Model::Serializable
attribute :id, :id
attribute :name, :string
xml do
element "product"
map_attribute "id", to: :id # ← XML attribute
map_element "name", to: :name # ← XML element
end
endGenerated XSD:
<xs:element name="product">
<xs:complexType>
<xs:sequence>
<xs:element name="name" type="xs:string"/>
</xs:sequence>
<xs:attribute name="id" type="xs:ID"/>
</xs:complexType>
</xs:element>Collections and Cardinality
class Catalog < Lutaml::Model::Serializable
attribute :products, Product, collection: 1.. # At least 1
xml do
element "catalog"
map_element "product", to: :products
end
endGenerated XSD:
<xs:element name="catalog">
<xs:complexType>
<xs:sequence>
<xs:element name="product" type="ProductType"
minOccurs="1" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
</xs:element>Collection Ranges:
collection: true # minOccurs="0" maxOccurs="unbounded"
collection: 1.. # minOccurs="1" maxOccurs="unbounded"
collection: 0..5 # minOccurs="0" maxOccurs="5"
collection: 3..10 # minOccurs="3" maxOccurs="10"Namespaces in XSD
Target Namespace
class ProductNamespace < Lutaml::Model::XmlNamespace
uri "https://example.com/product"
prefix_default "prod"
element_form_default :qualified
end
class Product < Lutaml::Model::Serializable
attribute :name, :string
xml do
element "product"
namespace ProductNamespace
map_element "name", to: :name
end
endGenerated XSD:
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="https://example.com/product"
xmlns:prod="https://example.com/product"
elementFormDefault="qualified">
<xs:element name="product" type="prod:ProductType"/>
</xs:schema>Type Namespaces
Value types can declare their own namespaces:
class CustomTypeNamespace < Lutaml::Model::XmlNamespace
uri "https://example.com/types"
prefix_default "ct"
end
class CustomIdType < Lutaml::Model::Type::String
xml_namespace CustomTypeNamespace
xsd_type "CustomID"
endThis generates import declarations in the schema.
Complete Example: E-Commerce Catalog
# Namespace definition
class CatalogNamespace < Lutaml::Model::XmlNamespace
uri "https://example.com/catalog"
prefix_default "cat"
element_form_default :qualified
documentation "E-commerce product catalog schema"
end
# Custom types
class ProductIdType < Lutaml::Model::Type::String
xsd_type 'xs:ID'
def self.cast(value)
id = value.to_s.strip.upcase
unless id.match?(/\APROD-\d+\z/)
raise Lutaml::Model::TypeError, "Invalid product ID: #{id}"
end
id
end
end
Lutaml::Model::Type.register(:product_id, ProductIdType)
# Models
class Money < Lutaml::Model::Serializable
attribute :amount, :float
attribute :currency, :string
xml do
type_name "MoneyType"
map_element "amount", to: :amount
map_attribute "currency", to: :currency
end
end
class Product < Lutaml::Model::Serializable
attribute :id, :product_id
attribute :name, :string
attribute :price, Money
attribute :tags, :string, collection: 0..
xml do
namespace CatalogNamespace
element "product"
type_name "ProductType"
map_attribute "id", to: :id
map_element "name", to: :name
map_element "price", to: :price
map_element "tag", to: :tags
end
end
class Catalog < Lutaml::Model::Serializable
attribute :products, Product, collection: 1..
xml do
namespace CatalogNamespace
element "catalog"
type_name "CatalogType"
map_element "product", to: :products
end
end
# Generate XSD
xsd = Lutaml::Model::Schema.to_xsd(Catalog)
File.write("catalog.xsd", xsd)Generated XSD (excerpt):
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="https://example.com/catalog"
xmlns:cat="https://example.com/catalog"
elementFormDefault="qualified">
<xs:element name="catalog" type="cat:CatalogType"/>
<xs:complexType name="CatalogType">
<xs:sequence>
<xs:element name="product" type="cat:ProductType"
minOccurs="1" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:complexType name="ProductType">
<xs:sequence>
<xs:element name="name" type="xs:string"/>
<xs:element name="price" type="cat:MoneyType"/>
<xs:element name="tag" type="xs:string"
minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="id" type="xs:ID" use="required"/>
</xs:complexType>
<xs:complexType name="MoneyType">
<xs:sequence>
<xs:element name="amount" type="xs:decimal"/>
</xs:sequence>
<xs:attribute name="currency" type="xs:string"/>
</xs:complexType>
</xs:schema>Schema Generation Options
# Basic generation
xsd = Lutaml::Model::Schema.to_xsd(MyModel)
# With options
xsd = Lutaml::Model::Schema.to_xsd(MyModel,
adapter: :nokogiri, # XML adapter
encoding: "UTF-8", # Character encoding
pretty: true # Format output
)
# Save to file
Lutaml::Model::Schema.to_xsd(MyModel,
output_dir: "schemas",
create_files: true
)Best Practices
Type Definition Strategy
-
Create reusable types for common patterns:
# Good: Reusable types
class IdType < Lutaml::Model::Type::String
xsd_type 'xs:ID'
end
class EmailType < Lutaml::Model::Type::String
xsd_type 'xs:string'
end
# Register for convenience
Lutaml::Model::Type.register(:id, IdType)
Lutaml::Model::Type.register(:email, EmailType)-
Use type-only models for shared structures:
# Good: Shared address format
class AddressType < Lutaml::Model::Serializable
attribute :street, :string
attribute :city, :string
xml do
type_name "AddressType" # Reusable by multiple elements
map_element "street", to: :street
map_element "city", to: :city
end
end-
Keep element declarations at top-level entry points:
# Good: Top-level catalog has element
class Catalog < Lutaml::Model::Serializable
xml do
element "catalog" # Entry point
type_name "CatalogType"
end
end
# Supporting types are type-only
class CatalogItemType < Lutaml::Model::Serializable
xml do
type_name "CatalogItemType" # No element
end
endCommon Patterns
ID/IDREF Pattern
class Category < Lutaml::Model::Serializable
attribute :id, :id
attribute :name, :string
xml do
element "category"
map_attribute "id", to: :id
map_element "name", to: :name
end
end
class Product < Lutaml::Model::Serializable
attribute :category_ref, :idref
xml do
element "product"
map_attribute "categoryRef", to: :category_ref
end
endTroubleshooting
Type Not Appearing in XSD
Problem: Custom type’s xsd_type not in generated schema.
Solution: Ensure type is actually used by a model attribute:
class IdType < Lutaml::Model::Type::String
xsd_type 'xs:ID' # Won't appear unless used
end
class Product < Lutaml::Model::Serializable
attribute :id, IdType # ← Must use the type
endNamespace Import Not Generated
Problem: Type namespace not imported in schema.
Solution: Use XmlNamespace class and set schema_location:
class TypeNamespace < Lutaml::Model::XmlNamespace
uri "https://example.com/types"
schema_location "types.xsd" # ← Enables import
endType Resolution and Validation
Overview
When generating XSD schemas, Lutaml::Model validates all xsd_type declarations to ensure they reference valid types. This prevents generation of invalid schemas with unresolvable type references.
Three Type Categories
Every xsd_type value falls into one of three categories:
| Category | Description | Example |
|---|---|---|
Standard XS Types | W3C XML Schema 1.1 Part 2 built-in types |
|
Custom Types | LutaML | Your custom types with proper definitions |
External Types | References to types not defined or resolvable | Error: Causes |
Standard XS Type Reference
LutaML recognizes all W3C XML Schema 1.1 Part 2 built-in types:
Primitive Types:
xs:string # Character strings
xs:boolean # true/false values
xs:decimal # Arbitrary precision decimals
xs:float # 32-bit floating point
xs:double # 64-bit floating point
xs:duration # Time duration (P1Y2M3DT4H5M6S)
xs:dateTime # Date and time (YYYY-MM-DDTHH:MM:SS)
xs:time # Time of day (HH:MM:SS)
xs:date # Calendar date (YYYY-MM-DD)
xs:gYearMonth # Year and month (YYYY-MM)
xs:gYear # Year (YYYY)
xs:gMonthDay # Month and day (--MM-DD)
xs:gDay # Day of month (---DD)
xs:gMonth # Month (--MM)
xs:hexBinary # Hex-encoded binary data
xs:base64Binary # Base64-encoded binary data
xs:anyURI # URI reference
xs:QName # Qualified name (prefix:localPart)
xs:NOTATION # Notation declarationDerived Types:
# String variants
xs:normalizedString # No line breaks/tabs
xs:token # Normalized, no extra whitespace
xs:language # Language identifier (en, en-US)
xs:NMTOKEN # XML name token
xs:NMTOKENS # Space-separated NMTOKEN list
xs:Name # XML name
xs:NCName # Non-colonized name
xs:ID # Unique identifier
xs:IDREF # Reference to ID
xs:IDREFS # Space-separated IDREF list
xs:ENTITY # Entity name
xs:ENTITIES # Space-separated ENTITY list
# Integer variants
xs:integer # Arbitrary precision integer
xs:nonPositiveInteger # ≤ 0
xs:negativeInteger # < 0
xs:long # -9223372036854775808 to 9223372036854775807
xs:int # -2147483648 to 2147483647
xs:short # -32768 to 32767
xs:byte # -128 to 127
xs:nonNegativeInteger # ≥ 0
xs:unsignedLong # 0 to 18446744073709551615
xs:unsignedInt # 0 to 4294967295
xs:unsignedShort # 0 to 65535
xs:unsignedByte # 0 to 255
xs:positiveInteger # > 0
# Duration variants
xs:yearMonthDuration # Year-month duration only
xs:dayTimeDuration # Day-time duration only
xs:dateTimeStamp # dateTime with required timezoneSpecial Types:
xs:anyType # Base for all types
xs:anySimpleType # Base for all simple typesValidation Process
XSD generation validates types automatically:
# This works - standard type
class Product < Lutaml::Model::Serializable
attribute :name, :string # xs:string is standard
xml do
element "product"
end
end
xsd = Lutaml::Model::Schema.to_xsd(Product) # ✓ Success# This fails - undefined custom type
class BadType < Lutaml::Model::Type::String
xsd_type "UndefinedType" # Not xs: prefixed, not defined
end
class Product < Lutaml::Model::Serializable
attribute :field, BadType
xml do
element "product"
end
end
# Raises: Lutaml::Model::UnresolvableTypeError
# Attribute 'field' uses unresolvable xsd_type 'UndefinedType'.
# Custom types must be defined as LutaML Type::Value or Model classes.
xsd = Lutaml::Model::Schema.to_xsd(Product) # ✗ ErrorCustom Type Resolution
Custom types are resolved through:
-
Type-only models with
type_name:
class AddressType < Lutaml::Model::Serializable
attribute :street, :string
xml do
type_name "AddressType" # ← Defines resolvable custom type
end
end
class Person < Lutaml::Model::Serializable
attribute :address, AddressType # ← Resolves to AddressType
end-
Type::Value classes with
xsd_typematching a model’stype_name:
# Define a model with type_name
class CustomType < Lutaml::Model::Serializable
xml do
type_name "CustomDataType"
end
end
# Value type referencing it
class CustomValueType < Lutaml::Model::Type::String
xsd_type "CustomDataType" # ← References CustomType's type_name
endValidation Errors
Error: Unresolvable custom type
# Bad: Type not defined anywhere
class UndefinedType < Lutaml::Model::Type::String
xsd_type "MysteryType" # Not xs:, not defined in models
end
class Model < Lutaml::Model::Serializable
attribute :field, UndefinedType
end
Lutaml::Model::Schema.to_xsd(Model)
# UnresolvableTypeError: Attribute 'field' uses unresolvable xsd_type 'MysteryType'.
# Custom types must be defined as LutaML Type::Value or Model classes.Fix: Define the type
# Good: Type defined as model
class MysteryType < Lutaml::Model::Serializable
xml do
type_name "MysteryType" # ← Now resolvable
end
end
class CustomType < Lutaml::Model::Type::String
xsd_type "MysteryType" # ← Can now resolve
endError: Nested model with unresolvable type
class BadType < Lutaml::Model::Type::String
xsd_type "BadCustomType"
end
class NestedModel < Lutaml::Model::Serializable
attribute :bad, BadType
end
class ParentModel < Lutaml::Model::Serializable
attribute :nested, NestedModel
end
Lutaml::Model::Schema.to_xsd(ParentModel)
# UnresolvableTypeError: In nested model NestedModel:
# Attribute 'bad' uses unresolvable xsd_type 'BadCustomType'.Skipping Validation
For development or special cases, validation can be skipped:
# Skip validation (use with caution)
xsd = Lutaml::Model::Schema.to_xsd(Model, skip_validation: true)Warning: Skipping validation may produce invalid XSD with unresolvable type references. Only use for:
-
Prototyping and development
-
Generated schemas to be manually edited
-
Schemas with external type definitions not visible to Lutaml
Best Practices
-
Always use standard xs: types when possible:
# Good: Standard types work everywhere attribute :id, :string # Uses xs:string attribute :count, :integer # Uses xs:integer -
Define custom types properly:
# Good: Custom type with standard base class EmailType < Lutaml::Model::Type::String xsd_type 'xs:string' # Uses standard type def self.cast(value) # Add validation end end -
Use type_name for reusable model types:
# Good: Reusable address type class AddressType < Lutaml::Model::Serializable xml do type_name "AddressType" # Makes it resolvable end end -
Check validation errors carefully:
They indicate real problems that would produce invalid XSD.