Value types

General types

Lutaml::Model supports the following attribute value types.

Every type has a corresponding Ruby class and a serialization format type.

Table 1. Mapping between Lutaml::Model::Type classes, Ruby equivalents and serialization format types
Lutaml::Model::Type Ruby class XML JSON YAML Example value

:string

String

xs:string

string

string

"text"

:integer

Integer

xs:integer

number

integer

42

:float

Float

xs:decimal

number

float

3.14

:boolean

TrueClass/FalseClass

xs:boolean

boolean

boolean

true, false

:symbol

Symbol

xs:string

string

symbol

:example (XML/JSON ":example:")

:date

Date

xs:date

string

string

2024-01-01 (JSON/YAML "2024-01-01")

:time_without_date

Time

xs:time

string

string

"12:34:56"

:date_time

DateTime

xs:dateTime

string

string

"2024-01-01T12:00:00+00:00"

:time

Time

xs:dateTime

string

string

"2024-01-01T12:00:00+00:00"

:decimal (optional)

BigDecimal

xs:decimal

number

float

123.45

:duration

Lutaml::Model::Type::Duration

xs:duration

string

string

"P1Y2M3DT4H5M6S"

:uri

String

xs:anyURI

string

string

"https://example.com"

:qname

Lutaml::Model::Type::QName

xs:QName

string

string

"prefix:localName"

:base64_binary

String

xs:base64Binary

string

string

"SGVsbG8gV29ybGQ="

:hex_binary

String

xs:hexBinary

string

string

"48656c6c6f"

:hash

Hash

complex element

object

map

{key: "value"}

(nil value)

nil

xs:anyType

null

null

null

{ ref: [Model, :attr] }

Reference

xs:string

string

string

"model-id"

Decimal type

Decimal is an optional feature.

The Decimal type is a value type that is disabled by default.

The reason why the Decimal type is disabled by default is that the BigDecimal class became optional to the standard Ruby library from Ruby 3.4 onwards. The Decimal type is only enabled when the bigdecimal library is loaded.

The following code needs to be run before using (and parsing) the Decimal type:

require 'bigdecimal'

If the bigdecimal library is not loaded, usage of the Decimal type will raise a Lutaml::Model::TypeNotSupportedError.

Additional XSD types

Lutaml::Model supports additional XSD types for specialized data handling:

Duration type

The :duration type handles ISO 8601 duration values conforming to xs:duration.

Duration format: P[n]Y[n]M[n]DT[n]H[n]M[n]S

Where,

P

Required prefix indicating period

[n]Y

Years (optional)

[n]M

Months (optional, before T)

[n]D

Days (optional)

T

Time prefix (required if time components present)

[n]H

Hours (optional)

[n]M

Minutes (optional, after T)

[n]S

Seconds (optional, can include decimals)

Example 1. Using the :duration type
class ProcessingTask < Lutaml::Model::Serializable
  attribute :processing_time, :duration

  xml do
    root "task"
    map_element "processingTime", to: :processing_time
  end
end

# Valid durations
task1 = ProcessingTask.new(processing_time: "P1Y2M3D")      # 1 year, 2 months, 3 days
task2 = ProcessingTask.new(processing_time: "PT4H5M6S")     # 4 hours, 5 minutes, 6 seconds
task3 = ProcessingTask.new(processing_time: "P1Y2M3DT4H5M6S")  # Combined
task4 = ProcessingTask.new(processing_time: "PT0.5S")       # 0.5 seconds

puts task1.to_xml
# => <task><processingTime>P1Y2M3D</processingTime></task>

URI type

The :uri type handles Uniform Resource Identifiers conforming to xs:anyURI.

Example 2. Using the :uri type
class Resource < Lutaml::Model::Serializable
  attribute :homepage, :uri
  attribute :schema_location, :uri

  xml do
    root "resource"
    map_element "homepage", to: :homepage
    map_attribute "schemaLocation", to: :schema_location
  end
end

resource = Resource.new(
  homepage: "https://example.com/page",
  schema_location: "https://example.com/schema.xsd"
)

puts resource.to_xml
# => <resource schemaLocation="https://example.com/schema.xsd">
#      <homepage>https://example.com/page</homepage>
#    </resource>

QName type

The :qname type handles XML qualified names conforming to xs:QName.

A QName consists of an optional namespace prefix and a local name, separated by a colon.

Example 3. Using the :qname type
class Reference < Lutaml::Model::Serializable
  attribute :ref_type, :qname
  attribute :target, :qname

  xml do
    root "reference"
    map_attribute "type", to: :ref_type
    map_element "target", to: :target
  end
end

ref = Reference.new(
  ref_type: "xsd:string",
  target: "ns:elementName"
)

puts ref.to_xml
# => <reference type="xsd:string">
#      <target>ns:elementName</target>
#    </reference>

# Accessing QName components
qname = Lutaml::Model::Type::QName.new("prefix:localName")
puts qname.prefix      # => "prefix"
puts qname.local_name  # => "localName"

Base64Binary type

The :base64_binary type handles base64-encoded binary data conforming to xs:base64Binary.

Example 4. Using the :base64_binary type
class Attachment < Lutaml::Model::Serializable
  attribute :content, :base64_binary
  attribute :filename, :string

  xml do
    root "attachment"
    map_element "content", to: :content
    map_attribute "filename", to: :filename
  end
end

# Encoding binary data
binary_data = "Hello World"
encoded = Lutaml::Model::Type::Base64Binary.encode(binary_data)

attachment = Attachment.new(
  content: encoded,
  filename: "hello.txt"
)

puts attachment.to_xml
# => <attachment filename="hello.txt">
#      <content>SGVsbG8gV29ybGQ=</content>
#    </attachment>

# Decoding
decoded = Lutaml::Model::Type::Base64Binary.decode(attachment.content)
# => "Hello World"

HexBinary type

The :hex_binary type handles hexadecimal-encoded binary data conforming to xs:hexBinary.

Example 5. Using the :hex_binary type
class Checksum < Lutaml::Model::Serializable
  attribute :hash_value, :hex_binary
  attribute :algorithm, :string

  xml do
    root "checksum"
    map_element "value", to: :hash_value
    map_attribute "algorithm", to: :algorithm
  end
end

# Encoding binary data
binary_data = "Hello"
encoded = Lutaml::Model::Type::HexBinary.encode(binary_data)

checksum = Checksum.new(
  hash_value: encoded,
  algorithm: "SHA256"
)

puts checksum.to_xml
# => <checksum algorithm="SHA256">
#      <value>48656c6c6f</value>
#    </checksum>

# Decoding
decoded = Lutaml::Model::Type::HexBinary.decode(checksum.hash_value)
# => "Hello"

Symbol type

The Symbol type provides support for Ruby symbols across all serialization formats.

Type Casting Behavior:

The Symbol type only accepts valid string inputs and existing symbols:

  • Non-empty strings: "active":active

  • Existing symbols: :pending:pending

  • Wrapper format: ":done:":done

  • Other types: integers, arrays, hashes, booleans → symbol (works similar to string type)

  • Empty strings: ""nil

Since not all serialization formats natively support symbols (XML, JSON, TOML don’t), the Symbol type uses format-specific serialization strategies:

  • YAML: Uses native symbol format (:symbol)

  • XML/JSON/TOML: Uses wrapper format (":symbol:") for compatibility

The Symbol type automatically handles conversion between these formats when parsing and serializing.

Example 6. Using symbols with different serialization formats
class Task < Lutaml::Model::Serializable
  attribute :status, :symbol
  attribute :priority, :symbol

  xml do
    root "task"
    map_element "status", to: :status
    map_element "priority", to: :priority
  end

  json do
    map "status", to: :status
    map "priority", to: :priority
  end
end

task = Task.new(status: :in_progress, priority: :high)

# XML serialization uses wrapper format
task.to_xml
# => <task><status>:in_progress:</status><priority>:high:</priority></task>

# JSON serialization uses wrapper format
task.to_json
# => {"status":":in_progress:","priority":":high:"}

# YAML serialization uses native symbols
task.to_yaml
# => ---
# => status: :in_progress
# => priority: :high

# All formats parse back to Ruby symbols correctly
Task.from_xml(task.to_xml).status  # => :in_progress
Task.from_json(task.to_json).status  # => :in_progress
Task.from_yaml(task.to_yaml).status  # => :in_progress

Custom types

General

A custom type is a user-defined class that extends the behavior of built-in types. A built-in type is one that is provided by Lutaml::Model, such as :string, :integer, or :date.

Understanding types and models

Lutaml::Model provides two approaches to define custom data structures:

Types

Defines primitive values that represent a basic unit of information. A type cannot be further decomposed. Inherits from Lutaml::Model::Type::Value.

Models

Defines objects composed of multiple attributes, of which each attribute can be a type or a model. Inherits from Lutaml::Model::Serializable.

The key differences are described in the table below.

Aspect Types (Type::Value) Models (Serializable)

Purpose

Represent single primitive-like values with custom behavior

Represent complex objects with multiple attributes and relationships

Storage

Contains a single value attribute

Contains multiple attributes defined via attribute declarations

Use cases

Value transformation, validation, format-specific serialization of primitives

Complex nested data structures, objects with multiple properties

Required methods

self.cast(value), self.serialize(value)

None (provided by framework)

Inheritance

< Lutaml::Model::Type::Value (or built-in types like Type::String)

< Lutaml::Model::Serializable

Registration

Can be registered as reusable types via Lutaml::Model::Type.register

Not registered as types, used directly as classes

Serialization control

Fine-grained control per format via to_xml, to_json, etc.

Controlled via mapping blocks (xml do, json do, etc.)

Quick reference: Type vs Model
# ✅ CUSTOM TYPE - for single values with special behavior
class PostCode < Lutaml::Model::Type::String
  def self.cast(value)
    value.to_s.upcase.gsub(/\s/, '') # Normalize: remove spaces, uppercase
  end
end

# ✅ MODEL - for complex objects with multiple attributes
class Address < Lutaml::Model::Serializable
  attribute :street, :string
  attribute :city, :string
  attribute :postal_code, PostCode  # Uses the custom type above
end

# Usage in a model
class Person < Lutaml::Model::Serializable
  attribute :name, :string           # Built-in type
  attribute :postal_code, PostCode   # Custom type (single value)
  attribute :address, Address        # Serializable object (multiple attributes)
end

When to use custom types and models

Use Custom Types when:

  • You need to transform or validate a single primitive value

  • You want consistent behavior across multiple attributes of the same type

  • You need format-specific serialization of primitive data

  • You’re creating reusable value types (like currency, phone numbers, postcodes)

  • The data represents a single conceptual value, even if complex internally

Use Models when:

  • You need to model objects with multiple attributes

  • You want to define relationships between objects

  • You need complex nested structures

  • The data represents a distinct entity or concept with multiple properties

  • You need different serialization mappings for the same object structure

Creating custom types

A custom class can be used as an attribute type. The custom class must inherit from Lutaml::Model::Type::Value or a class that inherits from it.

A class inheriting from the Value class carries the attribute value which stores the one-and-only "true" value that is independent of serialization formats.

The minimum requirement for a custom class is to implement the following methods:

self.cast(value)

Assignment of an external value to the Value class to be set as value. Casts the value to the custom type.

self.serialize(value)

Serializes the custom type to an object (e.g. a string). Takes the internal value and converts it into an output suitable for serialization.

Example 7. Using a custom value type to normalize a postcode with minimal methods
class FiveDigitPostCode < Lutaml::Model::Type::String
  def self.cast(value)
    value = value.to_s if value.is_a?(Integer)

    unless value.is_a?(::String)
      raise Lutaml::Model::TypeError, "Invalid value for type 'FiveDigitPostCode'"
    end

    # Pad zeros to the left
    value.rjust(5, '0')
  end

  def self.serialize(value)
    value
  end
end

class Studio < Lutaml::Model::Serializable
  attribute :postcode, FiveDigitPostCode
end

Practical examples: Type vs Model

Example 8. Custom Type for currency values

Use a custom type when you need to handle currency with consistent formatting and validation:

# Custom Type - represents a single currency value
class Currency < Lutaml::Model::Type::Value
  def self.cast(value)
    case value
    when String
      # Remove currency symbols and convert to float
      cleaned = value.gsub(/[$,]/, '')
      Float(cleaned)
    when Numeric
      value.to_f
    else
      raise Lutaml::Model::TypeError, "Invalid currency value: #{value}"
    end
  end

  def self.serialize(value)
    sprintf("%.2f", value)
  end

  # Format-specific serialization
  def to_xml
    "$#{sprintf('%.2f', value)}"
  end

  def to_json(*_args)
    value # JSON uses numbers
  end
end

class Product < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :price, Currency      # Reusable custom type
  attribute :wholesale_price, Currency  # Same type, consistent behavior
end
Example 9. Serializable object for complex data

Use a Serializable object when you need multiple related attributes:

# Serializable object - represents a complex address with multiple attributes
class Address < Lutaml::Model::Serializable
  attribute :street, :string
  attribute :city, :string
  attribute :postal_code, :string
  attribute :country, :string

  # Define how this complex object maps to different formats
  xml do
    root "Address"
    map_element "Street", to: :street
    map_element "City", to: :city
    map_element "PostalCode", to: :postal_code
    map_element "Country", to: :country
  end

  json do
    map "street", to: :street
    map "city", to: :city
    map "postalCode", to: :postal_code
    map "country", to: :country
  end
end

class Studio < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :address, Address  # Complex nested object
end
Example 10. When to choose each approach
# GOOD: Custom Type for phone numbers (single conceptual value)
class PhoneNumber < Lutaml::Model::Type::String
  def self.cast(value)
    # Normalize phone number format
    value.to_s.gsub(/\D/, '') # Remove non-digits
  end

  def to_xml
    # Format for XML: +1-555-123-4567
    "+1-#{value[0..2]}-#{value[3..5]}-#{value[6..9]}"
  end
end

# GOOD: Serializable for contact info (multiple related attributes)
class ContactInfo < Lutaml::Model::Serializable
  attribute :email, :string
  attribute :phone, PhoneNumber  # Uses custom type
  attribute :preferred_contact_method, :string
end

# BAD: Don't use Serializable for simple values
class BadPhoneNumber < Lutaml::Model::Serializable
  attribute :number, :string  # Overkill for a single value
end

# BAD: Don't use Type for complex structures
class BadContactInfo < Lutaml::Model::Type::Value
  # This would be difficult to manage and serialize properly
  def self.cast(value)
    # Complex parsing logic for multiple fields... ❌
  end
end

Registering custom types

Custom types can be registered for reuse across your application using symbols:

# Register the custom type
Lutaml::Model::Type.register(:currency, Currency)
Lutaml::Model::Type.register(:phone, PhoneNumber)

# Now you can use symbols instead of class names
class Product < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :price, :currency      # Uses registered Currency type
  attribute :contact_phone, :phone  # Uses registered PhoneNumber type
end

# You can also look up registered types
currency_type = Lutaml::Model::Type.lookup(:currency)
# => Currency

Custom types with XSD types

Define custom types with XSD type declarations for schema generation:

Example 11. Custom type with XSD type declaration
class ProductId < Lutaml::Model::Type::String
  xsd_type 'xs:ID'  (1)

  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 Product < Lutaml::Model::Serializable
  attribute :id, ProductId  (2)
  attribute :name, :string

  xml do
    element 'product'
    map_attribute "id", to: :id
    map_element "name", to: :name
  end
end
1 Declare XSD type for schema generation
2 Attribute automatically uses ProductId’s xsd_type

For comprehensive xsd_type documentation, see Value Types - XSD Type Declaration.

For comprehensive XSD generation documentation including the three XSD patterns, see Creating XSD Schemas Guide.

Type casting and validation

Custom types automatically handle type casting and can include validation:

class TemperatureInCelsius < Lutaml::Model::Type::Integer
  def self.cast(value)
    temp = super(value) # Use parent's casting first

    # Add validation
    if temp < -273 || temp > 5000
      raise Lutaml::Model::TypeError,
            "Temperature #{temp}°C is outside valid range (-273 to 5000)"
    end

    temp
  end

  def to_xml
    "#{value}°C"
  end
end

class KilnSettings < Lutaml::Model::Serializable
  attribute :firing_temperature, TemperatureInCelsius
end

# Usage
kiln = KilnSettings.new(firing_temperature: "1200")  # String gets cast to Integer
# => #<KilnSettings @firing_temperature=1200>

# Invalid values raise errors
kiln = KilnSettings.new(firing_temperature: "-300")
# => Lutaml::Model::TypeError: Type Error: Temperature -300°C is outside valid range

Extending built-in types

You can extend existing built-in types to add custom behavior:

# Extend String type for email validation
class EmailAddress < Lutaml::Model::Type::String
  EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i

  def self.cast(value)
    email = super(value) # Use String's casting

    unless email.match?(EMAIL_REGEX)
      raise Lutaml::Model::TypeError, "Invalid email format: #{email}"
    end

    email.downcase # Normalize to lowercase
  end
end

# Extend Integer type for ID validation
class PositiveInteger < Lutaml::Model::Type::Integer
  def self.cast(value)
    num = super(value) # Use Integer's casting

    if num <= 0
      raise Lutaml::Model::TypeError, "Value must be positive: #{num}"
    end

    num
  end
end

# Extend Date type for business days only
class BusinessDate < Lutaml::Model::Type::Date
  def self.cast(value)
    date = super(value) # Use Date's casting

    if date.saturday? || date.sunday?
      raise Lutaml::Model::TypeError, "Business date cannot be weekend: #{date}"
    end

    date
  end
end

Custom type inheritance hierarchy

# Base currency type
class Currency < Lutaml::Model::Type::Value
  def self.cast(value)
    case value
    when String
      Float(value.gsub(/[^0-9.-]/, ''))
    when Numeric
      value.to_f
    end
  end

  def self.serialize(value)
    sprintf("%.2f", value)
  end
end

# Specialized currency types
class USDollar < Currency
  def to_xml
    "$#{sprintf('%.2f', value)}"
  end
end

class Euro < Currency
  def to_xml
    "€#{sprintf('%.2f', value)}"
  end
end

class JPYen < Currency
  def self.serialize(value)
    value.to_i.to_s  # No decimal places for Yen
  end

  def to_xml
    #{value.to_i}"
  end
end

# Register specialized types
Lutaml::Model::Type.register(:usd, USDollar)
Lutaml::Model::Type.register(:eur, Euro)
Lutaml::Model::Type.register(:jpy, JPYen)

class InternationalProduct < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :price_usd, :usd
  attribute :price_eur, :eur
  attribute :price_jpy, :jpy
end

Serialization of custom types

The serialization of custom types can be made to differ per serialization format by defining methods in the class definitions. This requires additional methods than the minimum required for a custom class (i.e. self.cast(value) and self.serialize(value)).

This is useful in the case when different serialization formats of the same model expect differentiated value representations.

The methods that can be overridden are named:

self.from_{format}(serialized_string)

Deserializes a string of the serialization format and returns the object to be assigned to the Value class' value.

to_{format}

Serializes the object to a string of the serialization format.

The {format} part of the method name is the serialization format in lowercase (e.g. hash, json, xml, yaml, toml).

Example 12. Using custom serialization methods to handle a high-precision date-time type

Suppose in XML we handle a high-precision date-time type that requires custom serialization methods, but other formats such as JSON do not support this type.

For instance, in the normal DateTime class, the serialized string is 2012-04-07T01:51:37+02:00, and the high-precision format is 2012-04-07T01:51:37.112+02:00.

We create HighPrecisionDateTime class is a custom class that inherits from Lutaml::Model::Type::DateTime.

class HighPrecisionDateTime < Lutaml::Model::Type::DateTime
  # Inherit the `self.cast(value)` and `self.serialize(value)` methods
  # from Lutaml::Model::Type::DateTime

  # The format looks like this `2012-04-07T01:51:37.112+02:00`
  def self.from_xml(xml_string)
    ::DateTime.parse(xml_string)
  end

  # The %L adds milliseconds to the time
  def to_xml
    value.strftime('%Y-%m-%dT%H:%M:%S.%L%:z')
  end
end

class Ceramic < Lutaml::Model::Serializable
  attribute :kiln_firing_time, HighPrecisionDateTime
  xml do
    root 'ceramic'
    map_element 'kilnFiringTime', to: :kiln_firing_time
    # ...
  end
end

An XML snippet with the high-precision date-time type:

<ceramic>
  <kilnFiringTime>2012-04-07T01:51:37.112+02:00</kilnFiringTime>
  <!-- ... -->
</ceramic>

When loading the XML snippet, the HighPrecisionDateTime class will be used to parse the high-precision date-time string.

However, when serializing to JSON, the value will have the high-precision part lost due to the inability of JSON to handle high-precision date-time.

> c = Ceramic.from_xml(xml)
> #<Ceramic:0x0000000104ac7240 @kiln_firing_time=#<HighPrecisionDateTime:0x0000000104ac7240 @value=2012-04-07 01:51:37.112000000 +0200>>
> c.to_json
> # {"kilnFiringTime":"2012-04-07T01:51:37+02:00"}

Best practices in using custom types

When implementing custom types:

  1. Ensure you have a clear use case: Decide between using a custom type or a model based on whether you need to represent a single value or a complex structure.

  2. Inherit appropriately: Use Lutaml::Model::Type::Value for completely new types, or extend built-in types like Type::String, Type::Integer for specialized behavior.

  3. Implement required methods: Always implement self.cast(value) and self.serialize(value) at minimum.

  4. Add validation: Use the cast method to validate and normalize input values.

  5. Handle format-specific serialization: Override to_xml, to_json, etc. methods when different formats need different representations.

  6. Register reusable types: Use Lutaml::Model::Type.register(:symbol, YourType) for types you’ll use across multiple models.

  7. Leverage inheritance: Create type hierarchies for related but specialized types.

  8. Test thoroughly: Custom types affect both parsing and serialization, so test with all formats you plan to use.

XSD type declaration

Purpose

Custom Value types can declare their XSD type representation for schema generation. This enables proper XSD schemas when using [Lutaml::Model::Schema.to_xsd](lib/lutaml/model/schema.rb).

When a custom type declares its xsd_type, that type information flows through to:

  • XSD schema generation via [Schema.to_xml](lib/lutaml/model/schema/xsd_schema.rb)

  • Type references in element and attribute declarations

  • Proper W3C XML Schema 1.1 compliance

The xsd_type class method

Use the xsd_type class method to declare the XSD type:

Example 13. Declaring XSD type for a custom Type class
class EmailType < Lutaml::Model::Type::String
  xsd_type 'xs:normalizedString'  (1)

  def self.cast(value)
    value.to_s.strip.gsub(/\s+/, ' ')
  end
end
1 Declares this type’s XSD representation

When generating XSD schema, attributes using EmailType will have type="xs:normalizedString".

Relationship to xsi:type in XML instances

The xsd_type declaration serves two purposes:

  1. Schema generation: Defines the type in generated XSD schemas

  2. Instance validation: Enables xsi:type references in XML instances

Example 14. Understanding xsd_type vs xsi:type
class SizeType < Lutaml::Model::Type::String
  xsd_type 'xs:token'  (1)
end

class Product < Lutaml::Model::Serializable
  attribute :size, SizeType

  xml do
    element 'product'
    map_element "size", to: :size
  end
end
1 Declares this type uses xs:token in schema definition

Generated XSD Schema:

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="product">
    <xs:complexType>
      <xs:sequence>
        <xs:element name="size" type="xs:token"/>  (1)
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>
1 The type="xs:token" comes from SizeType.xsd_type

XML Instance Example:

<product>
  <size>large</size>  (1)
  <size xsi:type="xs:string">1</size>  (2)
</product>
1 Normal instance conforming to declared xs:token type
2 Instance explicitly specifying type via xsi:type attribute

The xsi:type attribute in XML instances allows runtime type specification, referencing type names declared in the schema.

Example 15. Custom type names and xsi:type
class SizesListType < Lutaml::Model::Type::Value
  xsd_type 'sizes'  (1)

  def self.cast(value)
    # Parse space-separated decimal list
    value.to_s.split.map(&:to_f)
  end

  def serialize(value)
    value.join(' ')
  end
end
1 Declares custom type name 'sizes'

Generated XSD Schema:

<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:simpleType name='sizes'>  (1)
    <xs:list itemType='xs:decimal'/>
  </xs:simpleType>
</xs:schema>
1 Custom type definition from xsd_type 'sizes'

XML Instance Example:

<cerealSizes xsi:type='sizes'> 8 10.5 12 </cerealSizes>  (1)
1 Instance uses xsi:type='sizes' to reference the custom type

XSD type inheritance

Child types inherit xsd_type from their parent Type class:

Example 16. XSD type inheritance in a type hierarchy
class NormalizedString < Lutaml::Model::Type::String
  xsd_type 'xs:normalizedString'
end

class Token < NormalizedString
  xsd_type 'xs:token'  (1)

  def self.cast(value)
    super.strip.gsub(/\s+/, ' ')
  end
end

class Language < Token
  xsd_type 'xs:language'  (2)

  def self.cast(value)
    super.downcase
  end
end
1 Token overrides parent’s xs:normalizedString
2 Language overrides parent’s xs:token

Each child can override the parent’s xsd_type or inherit it.

Default XSD types

All built-in types have default XSD type mappings:

Table 2. Default XSD type mappings for built-in Type classes
Lutaml Type Default XSD Type

[Type::String](lib/lutaml/model/type/string.rb)

xs:string

[Type::Integer](lib/lutaml/model/type/integer.rb)

xs:integer

[Type::Float](lib/lutaml/model/type/float.rb)

xs:float

[Type::Boolean](lib/lutaml/model/type/boolean.rb)

xs:boolean

[Type::Date](lib/lutaml/model/type/date.rb)

xs:date

[Type::DateTime](lib/lutaml/model/type/date_time.rb)

xs:dateTime

[Type::Time](lib/lutaml/model/type/time.rb)

xs:time

[Type::Duration](lib/lutaml/model/type/duration.rb)

xs:duration

[Type::Decimal](lib/lutaml/model/type/decimal.rb)

xs:decimal

[Type::Uri](lib/lutaml/model/type/uri.rb)

xs:anyURI

[Type::QName](lib/lutaml/model/type/qname.rb)

xs:QName

[Type::Base64Binary](lib/lutaml/model/type/base64_binary.rb)

xs:base64Binary

[Type::HexBinary](lib/lutaml/model/type/hex_binary.rb)

xs:hexBinary

[Type::Hash](lib/lutaml/model/type/hash.rb)

xs:anyType

[Type::Symbol](lib/lutaml/model/type/symbol.rb)

xs:string

Custom types inherit these defaults and can override as needed.

Common XSD type examples

ID type

XML IDs must follow NCName rules:

Example 17. Creating an ID type with XSD type declaration
class IdType < Lutaml::Model::Type::String
  xsd_type 'xs:ID'

  def self.cast(value)
    id = super.strip
    unless id.match?(/\A[A-Za-z_][\w.-]*\z/)
      raise Lutaml::Model::TypeError, "Invalid XML ID: #{id}"
    end
    id
  end
end

class Product < Lutaml::Model::Serializable
  attribute :id, IdType

  xml do
    element 'product'
    map_attribute "id", to: :id
  end
end

Generated XSD will include <xs:attribute name="id" type="xs:ID"/>.

Token type

Tokens normalize whitespace:

Example 18. Creating a Token type with XSD type declaration
class TokenType < Lutaml::Model::Type::String
  xsd_type 'xs:token'

  def self.cast(value)
    super.strip.gsub(/\s+/, ' ')
  end
end

class Document < Lutaml::Model::Serializable
  attribute :category, TokenType

  xml do
    element 'document'
    map_element "category", to: :category
  end
end

Language code type

Language codes follow RFC 5646:

Example 19. Creating a Language type with XSD type declaration
class LanguageType < Lutaml::Model::Type::String
  xsd_type 'xs:language'

  def self.cast(value)
    lang = super.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 Metadata < Lutaml::Model::Serializable
  attribute :language, LanguageType

  xml do
    element 'metadata'
    map_attribute "lang", to: :language
  end
end

Element-level override

You can override a Type’s xsd_type for specific elements in the mapping:

Example 20. Overriding XSD type at the element level
class MyType < Lutaml::Model::Type::String
  xsd_type 'xs:normalizedString'
end

class Model < Lutaml::Model::Serializable
  attribute :field1, MyType
  attribute :field2, MyType

  xml do
    element 'model'
    map_element "field1", to: :field1, xsd_type: 'xs:token'  (1)
    map_element "field2", to: :field2  (2)
  end
end
1 Overrides MyType’s `xs:normalizedString with xs:token
2 Uses MyType’s default `xs:normalizedString

Precedence rules

When determining XSD type for schema generation:

  1. Element-level xsd_type in mapping (highest priority)

  2. Type-level xsd_type from Type class

  3. default_xsd_type from Type class (fallback)

This allows flexible type reuse with context-specific overrides.

Example 21. XSD type precedence example
class CustomType < Lutaml::Model::Type::String
  xsd_type 'xs:normalizedString'  (1)
end

class Model < Lutaml::Model::Serializable
  attribute :field1, CustomType
  attribute :field2, CustomType

  xml do
    element 'model'
    map_element "field1", to: :field1, xsd_type: 'xs:ID'  (2)
    map_element "field2", to: :field2  (3)
  end
end
1 Type-level declaration
2 Element-level override (highest priority)
3 Inherits Type-level declaration

Generated XSD:

<xs:element name="model">
  <xs:complexType>
    <xs:sequence>
      <xs:element name="field1" type="xs:ID"/>  <!-- Element-level override -->
      <xs:element name="field2" type="xs:normalizedString"/>  <!-- Type-level -->
    </xs:sequence>
  </xs:complexType>
</xs:element>

See also