Model definition

General

A LutaML model is used to represent a class of information, of which a model instance is a set of information representing a coherent concept.

There are two ways to define an information model in Lutaml::Model:

  • Inheriting from the Lutaml::Model::Serializable class

  • Including the Lutaml::Model::Serialize module

Definition

Through inheritance

The simplest way to define a model is to create a class that inherits from Lutaml::Model::Serializable.

The attribute class method is used to define attributes.

require 'lutaml/model'

class Kiln < Lutaml::Model::Serializable
  attribute :brand, :string
  attribute :capacity, :integer
  attribute :temperature, :integer
end

Through inclusion

If the model class already has a super class that it inherits from, the model can be extended using the Lutaml::Model::Serialize module.

require 'lutaml/model'

class Kiln < SomeSuperClass
  include Lutaml::Model::Serialize

  attribute :brand, :string
  attribute :capacity, :integer
  attribute :temperature, :integer
end

Inheritance

A model can inherit from another model to inherit all attributes and methods of the parent model, allowing for code reusability and a clear model hierarchy.

Syntax:

class Superclass < Lutaml::Model::Serializable
  # attribute ...
  # serialization blocks
end

class Subclass < Superclass
  # attributes are additive
  # serialization blocks are replaced
end

An inherited model has the following characteristics:

  • All attributes are inherited from the parent model.

  • Additional calls to attribute in the child model are additive, unless the attribute name is the same as an attribute in the parent model.

  • Serialization blocks, such as xml and key_value are replaced when defined.

    • In order to selectively import serialization mapping rules from the parent model, the import_model_mappings method can be used (see import_model_mappings).

Comparison and diff

A Serialize / Serializable object can be compared with another object of the same class using the == operator. This is implemented through the ComparableModel module, which provides powerful comparison and diff functionality.

Basic comparison

Two objects are considered equal if they have the same class and all their attributes are equal. This behavior differs from the typical Ruby behavior, where two objects are considered equal only if they have the same object ID.

Two Serialize objects will have the same hash value if they have the same class and all their attributes are equal.
> a = Kiln.new(brand: 'Kiln 1', capacity: 100, temperature: 1050)
> b = Kiln.new(brand: 'Kiln 1', capacity: 100, temperature: 1050)
> a == b
> # true
> a.hash == b.hash
> # true

Deep comparison

The comparison works recursively for nested objects and handles complex data structures:

class Glaze < Lutaml::Model::Serializable
  attribute :color, :string
  attribute :temperature, :integer
  attribute :food_safe, :boolean
end

class Ceramic < Lutaml::Model::Serializable
  attribute :type, :string
  attribute :glaze, Glaze
end

# Nested object comparison
glaze1 = Glaze.new(color: "Blue", temperature: 1200, food_safe: true)
glaze2 = Glaze.new(color: "Blue", temperature: 1200, food_safe: true)
ceramic1 = Ceramic.new(type: "Bowl", glaze: glaze1)
ceramic2 = Ceramic.new(type: "Bowl", glaze: glaze2)

ceramic1 == ceramic2  # true - deep comparison of nested objects

Circular reference handling

The comparison safely handles circular references without infinite loops:

class RecursiveNode < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :next_node, RecursiveNode
end

node1 = RecursiveNode.new(name: "A")
node2 = RecursiveNode.new(name: "B", next_node: node1)
node1.next_node = node2  # Creates circular reference

# Comparison still works without infinite loops
node1_copy = RecursiveNode.new(name: "A")
node2_copy = RecursiveNode.new(name: "B", next_node: node1_copy)
node1_copy.next_node = node2_copy

node1 == node1_copy  # true

Diff generation

The ComparableModel module provides powerful diff functionality to visualize differences between objects and calculate similarity scores.

Understanding diff and score

Diff (Difference): A visual representation showing the differences between two objects:

  • Removed values are marked with - (typically red)

  • Added values are marked with + (typically green)

  • Hierarchical structure shows nested objects and their relationships

Score: A numerical value between 0 and 1 representing how different the objects are:

  • 0 = Objects are identical (no differences)

  • 1 = Objects are completely different

  • 0.5 = Objects are 50% different

  • Convert to similarity percentage: (1 - score) * 100

The diff_with_score method

The diff_with_score method returns two values as an array:

diff_score, diff_tree = Lutaml::Model::Serialize.diff_with_score(obj1, obj2, options)
#    ↑           ↑
#  Float      String
# (0.0-1.0)   (Visual representation)
  1. diff_score (Float): The numerical difference score between 0.0 and 1.0

  2. diff_tree (String): The formatted visual diff showing all differences

Basic example
# Create two different objects
ceramic1 = Ceramic.new(
  type: "Bowl",
  glaze: Glaze.new(color: "Blue", temperature: 1200, food_safe: true)
)

ceramic2 = Ceramic.new(
  type: "Bowl",
  glaze: Glaze.new(color: "Red", temperature: 1000, food_safe: false)
)

# Generate diff with similarity score
diff_score, diff_tree = Lutaml::Model::Serialize.diff_with_score(ceramic1, ceramic2)

puts "Difference Score: #{diff_score}"
puts "Similarity: #{((1 - diff_score) * 100).round(2)}%"
puts "Visual Diff:"
puts diff_tree

Output:

Difference Score: 0.25
Similarity: 75.0%
Visual Diff:
└── Ceramic
    └── glaze (Glaze):
        ├── color (Lutaml::Model::Type::String):
        │   ├── - (String) "Blue"
        │   └── + (String) "Red"
        ├── temperature (Lutaml::Model::Type::Integer):
        │   ├── - (Integer) 1200
        │   └── + (Integer) 1000
        └── food_safe (Lutaml::Model::Type::Boolean):
            ├── - (TrueClass) true
            └── + (FalseClass) false
Understanding the return values structure

The method returns an array with exactly two elements:

result = Lutaml::Model::Serialize.diff_with_score(obj1, obj2)
# result[0] => diff_score (Float between 0.0 and 1.0)
# result[1] => diff_tree (String with visual representation)

# Or using array destructuring:
score, tree = Lutaml::Model::Serialize.diff_with_score(obj1, obj2)
puts "Objects are #{(score * 100).round(1)}% different"
puts tree
Diff options

The diff functionality supports various options:

# Show unchanged attributes as well
diff_score, diff_tree = Lutaml::Model::Serialize.diff_with_score(
  ceramic1,
  ceramic2,
  show_unchanged: true,      # Show attributes that are the same
  highlight_diff: false,     # Don't highlight only differences
  use_colors: true          # Use color coding (red/green)
)

# With indentation
diff_score, diff_tree = Lutaml::Model::Serialize.diff_with_score(
  ceramic1,
  ceramic2,
  indent: "  "
)
Collection handling

The diff functionality handles collections (arrays) intelligently:

class CeramicCollection < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :items, Ceramic, collection: true
end

collection1 = CeramicCollection.new(
  name: "Blue Collection",
  items: [
    Ceramic.new(type: "Bowl", glaze: glaze1),
    Ceramic.new(type: "Plate", glaze: glaze1)
  ]
)

collection2 = CeramicCollection.new(
  name: "Mixed Collection",
  items: [
    Ceramic.new(type: "Bowl", glaze: glaze1),  # Same as first
    Ceramic.new(type: "Cup", glaze: glaze2)     # Different
  ]
)

diff_score, diff_tree = Lutaml::Model::Serialize.diff_with_score(collection1, collection2)
# Shows detailed diff of collection items with indexes

Use cases

The comparison and diff functionality is particularly useful for:

  • Testing: Verify model equality in specs

  • Data Migration: Compare objects before/after transformation

  • Auditing: Track changes to model instances

  • Data Validation: Identify differences in data imports

  • Debugging: Visualize object differences during development

  • API Testing: Compare expected vs actual responses