Breaking changes in Lutaml::Model

General

This document lists breaking changes introduced in each version of Lutaml::Model.

v0.8.0: Namespace Restructuring

Version 0.8.0 is a major refactoring release that restructures namespaces for better organization.

A comprehensive migration guide is provided at v0.8.0 Migration Guide: Namespace Restructuring.

Key Changes

  1. Namespace restructuring: Format-specific code moved to dedicated namespaces

    Old Namespace New Namespace

    Lutaml::Model::Xml

    Lutaml::Xml

    Lutaml::Model::Json

    Lutaml::Json

    Lutaml::Model::Yaml

    Lutaml::Yaml

    Lutaml::Model::Toml

    Lutaml::Toml

    Lutaml::Model::Hash

    Lutaml::HashFormat

  2. Default adapters: XML defaults to :nokogiri, other formats have defaults

  3. TOML on Windows: :tomlib adapter disabled due to segfaults; use :toml_rb

  4. Attribute name conflicts: No more errors, only warnings

Migration

For most users, update your configuration:

# Old (v0.7.x)
config.xml_adapter = Lutaml::Model::Xml::NokogiriAdapter

# New (v0.8.0) - use symbols (recommended)
config.xml_adapter_type = :nokogiri

v0.9.0: Type-only models by default

Version 0.9.0 introduces a breaking change to XML root element auto-generation behavior.

What changed

Models without an explicit element() (or root()) declaration are now type-only models by default, instead of auto-generating a root element from the class name.

Before (v0.8.x)

class Address < Lutaml::Model::Serializable
  attribute :street, :string
  attribute :city, :string

  xml do
    map_element 'street', to: :street
    map_element 'city', to: :city
  end
end

# Auto-generated root element from class name
address.to_xml  # => <Address><street>...</street><city>...</city></Address>

After (v0.9.0)

class Address < Lutaml::Model::Serializable
  attribute :street, :string
  attribute :city, :string

  xml do
    # No element declaration - type-only model
    map_element 'street', to: :street
    map_element 'city', to: :city
  end
end

# Type-only models cannot be serialized standalone
address.to_xml  # => NoRootMappingError

Migration

For models that need a root element, add an explicit element() declaration:

class Address < Lutaml::Model::Serializable
  attribute :street, :string
  attribute :city, :string

  xml do
    element 'address'  # Add explicit element declaration
    map_element 'street', to: :street
    map_element 'city', to: :city
  end
end

address.to_xml  # => <address><street>...</street><city>...</city></address>

For type-only models (embedded in other models), no changes are needed - they now work without the deprecated no_root directive.

Why this change

  • Clearer intent: Explicit element declarations make the model’s purpose clear

  • Type-only by default: Models used as embedded types don’t need no_root

  • Consistency: All models with root elements have explicit declarations

  • Less magic: No auto-generation from class names

Deprecated: no_root method

The no_root method is deprecated but still works with a warning:

xml do
  no_root  # DEPRECATED - shows warning
  map_element 'street', to: :street
end

Simply remove the no_root call - models without element() are type-only by default.

v0.9.0: Strict cardinality validation

Attributes now enforce their declared cardinality. An attribute without collection: holds a single value, and giving it several is a violation that #validate reports. See Validation for the full rules.

Four behavior changes come with it.

Nested models now run their own validations

#validate recurses into nested models and validates each one. Polymorphic, required and custom checks on a nested model ran nowhere before; they run now. A model that validated clean may start reporting errors its children always had.

Singular readers can return arrays

An attribute given more values than it was declared for keeps them all, so #validate has something to report. Until the violation is fixed, the reader returns an array rather than a single value.

obj = Person.from_xml("<person><name>A</name><name>B</name></person>")
obj.name      # => ["A", "B"]
obj.validate  # => [#<Lutaml::Model::CollectionTrueMissingError ...>]

Code that assumed a singular reader always returns a scalar should call #validate before trusting the shape.

While the violation stands the document does not round-trip: serializing writes the array out as a single stringified value. Fix the cardinality and output returns to normal.

Enum shorthand predicates answer false while over-counted

The generated value? methods read through the same reader, so an attribute holding two values answers false for all of them.

obj = Doc.from_json('{"kind": ["draft", "final"]}')
obj.draft?  # => false, the attribute holds two values
obj.kind = "draft"
obj.draft?  # => true

Assigning a scalar to a collection attribute wraps it

Assignment now produces the same shape as parsing.

doc.tags = "one"
doc.tags  # => ["one"]

v0.8.0: XML default namespace behavior

Version 0.8.0 introduces a breaking change to XML namespace serialization behavior to align with W3C XML standards.

The default output format has changed from prefixed namespaces to default namespaces for cleaner, more standards-compliant XML.

A migration guide is provided at XML default namespace behavior migration guide.