Published

Ribose Standard

RS 3003
LutaML — Transforms
LutaML
Ribose Standard

Published 2025-02-20

Classification: unrestricted




1.  Scope

This document specifies the transformation capabilities in LutaML Model, which enable mapping between different model representations while maintaining data integrity and structure.

It defines:

  • Value transformation interfaces

  • Model transformation patterns

  • Nested transformation rules

  • Bidirectional transformation capabilities

2.  Normative references

There are no normative references in this document.

3.  Terms and definitions

For the purposes of this document, the following terms and definitions apply.

3.1. transform

operation that defines mapping rules between different model representations

3.2. value transform

transform (3.1) that operates on individual attribute values

3.3. model transform

transform (3.1) that operates on entire model structures

3.4. source model

model instance containing the original data to be transformed

3.5. target model

model instance that will receive the transformed data

4.  Principles in transformations

4.1.  General

A LutaML model defines the internal information organization structure of an information model.

In order to allow external users to interact with the model, it is necessary to provide a way to transform the defined model into other models:

  • Serialization models that represent the model in a specific serialization format. e.g. JSON, XML, YAML.

  • Other LutaML models that represent information differently.

  • Information models in another modelling language that represent information differently.

    NOTE  The “transform” referred here is a mapping between a source LutaML model and a target LutaML model.

4.2.  Architecture

In LutaML, a transform is a first-order object that defines:

  • source model

  • target model

  • mapping between the two models

  • any value transforms or processing logic

A transform can be unidirectional or bidirectional.

╔═══════════════════════╗   ╔══════════════════╗   ╔═══════════════════════╗
║LutaML Model Class FOO ║   ║LutaML Transformer║   ║LutaML Model Class BAR ║
╚═══════════════════════╝   ╚══════════════════╝   ╚═══════════════════════╝

╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮                          ╭┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╮
┆          Model        ┆                          ┆          Model        ┆
┆            │          ┆    ┌────────────────┐    ┆            │          ┆
┆   ┌────────┴──┐       ┆    │                │    ┆   ┌────────┴──┐       ┆
┆   │           │       ┆    │     Model      │    ┆   │           │       ┆
┆ Models   Value Types  ┆───►│ Transformation │───►┆ Models   Value Types  ┆
┆   │           │       ┆◄───│       &        │◄───┆   │           │       ┆
┆   │           │       ┆    │ Mapping Rules  │    ┆   │           │       ┆
┆   │    ┌──────┴──┐    ┆    │                │    ┆   │    ┌──────┴──┐    ┆
┆   │    │         │    ┆    └────────────────┘    ┆   │    │         │    ┆
┆   │   String  Integer ┆                          ┆   │   String  Integer ┆
┆   │   Date    Float   ┆                          ┆   │   Date    Float   ┆
┆   │   Time    Boolean ┆                          ┆   │   Time    Boolean ┆
┆   │                   ┆                          ┆   │                   ┆
┆   └──────┐            ┆                          ┆   └──────┐            ┆
┆          │            ┆                          ┆          │            ┆
┆     Contains          ┆                          ┆     Contains          ┆
┆     more Models       ┆                          ┆     more Models       ┆
┆     (recursive)       ┆                          ┆     (recursive)       ┆
┆                       ┆                          ┆                       ┆
╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯                          ╰┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄╯

Figure 1 — Model transformation of a LutaML Model to another LutaML Model

4.3.  Model and value transforms

A value transform is a transformation that operates on individual attribute values, converting from one type or format to another.

Common cases include:

  • Converting between different data types (e.g., string to date)

  • Splitting or combining values

EXAMPLE 1

Transforming:

  • source: a string date “2025-03-15”

  • target: a LutaML Value type “Date”.

A model transform operates on entire model structures, allowing for comprehensive mapping of various attributes and relations within the model.

Common cases include:

  • Converting between different model representations

  • Handling nested object transformations

  • Processing collections

EXAMPLE 2

Transforming:

  • source: a model with attributes title, author, publication_date

  • target: a model with attributes name, creator, date, which correspond to the source model attributes.

A source or target model here could also be a value type. For instance, extracting structured information (a model) from a string (a value type).

EXAMPLE 3

Transforming:

  • source: a string set of names "Gottlieb;von;Peter;Arnold"

  • target: a structured name model that contains given names ( ["Peter", "Arnold"]), conjunction ("von") and last name ("Gottlieb").

4.4.  Directionality

A transform can operate in one or both directions.

A unidirectional transform is a transformation that operates in one direction only, and cannot be reversed.

A bidirectional transform is a transformation that operates in both directions, allowing for reversible transformations.

4.5.  Common use cases

4.5.1.  Between model representations

This use case is demonstrated by the Plurimath gem’s handling of MathML conversion ( plurimath/plurimath#304).

In Plurimath:

  • Plurimath maintains an internal math model (the Formula class) for mathematical semantics

  • The mml gem models the MathML language specification, and provides MathML XML serialization

When a MathML XML document is loaded, it is transformed into a Plurimath::Formula model instance in these steps:

  1. MathML XML ⇒ The Mml LutaML model within the mml gem

  2. The Mml LutaML model ⇒ The Plurimath::Formula LutaML model

This architecture means Plurimath::Math does not directly handle serialization, but can transform into the Mml model when serialization is needed.

When a MathML XML document is saved, the process is reversed:

  1. The Plurimath::Formula LutaML model ⇒ The Mml LutaML model

  2. The Mml LutaML model within the mml gem ⇒ MathML XML

4.5.2.  Between serialization models

A common requirement is the need to handle multiple serialization formats for the same data model.

The modspec gem provides a LutaML model for the OGC Modular Specification (ModSpec) requirements model, and supports XML and YAML serialization outputs (the “Native ModSpec XML/YAML format”).

The mn-requirements gem needs to provide a Metanorma Requirements XML serialization format for the identical ModSpec model (the “Metanorma Requirements XML format”).

In encoding Metanorma Requirements in ModSpec, the user supplies Native ModSpec YAML which is meant to be transformed into Metanorma Requirements XML.

The transformation process is:

  1. ModSpec YAML ⇒ ModSpec LutaML model

  2. ModSpec LutaML model ⇒ Metanorma Requirements LutaML model

  3. Metanorma Requirements LutaML model ⇒ Metanorma Requirements XML (the “Native Metanorma Requirements XML format”)

In reverse, when the user wants to extract ModSpec YAML from Metanorma Requirements XML:

  1. Metanorma Requirements XML ⇒ Metanorma Requirements LutaML model

  2. Metanorma Requirements LutaML model ⇒ ModSpec LutaML model

  3. ModSpec LutaML model ⇒ ModSpec YAML

4.5.3.  Between versioned models

A common use case involves transforming between different versions of the same model as it evolves over time.

The Relaton LutaML model demonstrates this pattern:

  • Model version information is stored in the schema-version attribute of serialized formats of Relaton.

  • When an older version of the Relaton serialization is parsed, it is first interpreted by the appropriate version of the Relaton serialization LutaML model, and then transformed into the latest version of the Relaton data model.

  • A version-to-version transform handles model changes

EXAMPLE

Relaton XML/YAML version attributes:

<bibdata type="standard" schema-version="1.2.9">
  ...
  <ext schema-version="1.0.3">
    ...
  </ext>
</bibdata>
id: ISO1231994
type: standard
schema_version: 1.2.9
...
ext:
  schema_version: 1.0.3
  ...

For transformations across multiple versions, transformations must be applied sequentially in historical order (e.g., “1.0.1” → “1.0.2” → “1.0.3”).

4.6.  Model transformation patterns

4.6.1.  General

There are several common model transformation patterns:

  • Generic-to-specific transformation

  • Specific-to-generic transformation

  • Many-to-many transformation

4.6.2.  Re-mapping attributes

When transforming between models, it is common to re-map attributes between different models without changing value types.

EXAMPLE

Converting a “title” attribute in a “Publication” model to a “name” attribute in a “CatalogEntry” model.

4.6.3.  Generic-to-specific transformation

Transforms a general model into a more specific one.

EXAMPLE

Converting a general “car” model into a specialized “taxi” model.

4.6.4.  Specific-to-generic transformation

Transforms a specific model into a more general one.

EXAMPLE

Converting a specialized “taxi” model into a general “car” model.

4.6.5.  Many-to-many transformation

Transforms a model that can be represented in multiple ways.

EXAMPLE

An amphibious vehicle model that can transform into both “car” and “boat” models.

4.7.  Directionality

4.7.1.  General

Transforms can be configured to operate in one or both directions.

The reversibility of a transform depends on two things:

  • whether any mapping rules are one-way transforms

  • whether the reverse_transform do block is defined

4.7.2.  Simple transforms (bidirectional)

When a transform is defined with only a transform do block that contains bidirectional mapping rules, the transform is bidirectional.

class SimpleBidirectionalTransform < Lutaml::Model::Transform
  source_model :source_model
  target_model :target_model

  transform do
    # mapping without value transform logic
  end
end

Figure 2

4.7.3.  Single direction transform

When a transform is defined with only a transform do block that contains unidirectional mapping rules, the transform is unidirectional.

class UnidirectionalTransform < Lutaml::Model::Transform
  source_model :source_model
  target_model :target_model

  transform do
    # mapping with value transform logic
  end
end

Figure 3

4.7.4.  Explicit bidirectional transform

When a transform is defined with a transform do block that contains unidirectional mapping rules, but also a reverse_transform do block that contains reverse unidirectional mapping rules, the transform is bidirectional.

class ExplicitBidirectionalTransform < Lutaml::Model::Transform
  source_model :source_model
  target_model :target_model

  transform do
    # mapping with value transform logic
  end

  reverse_transform do
    # mapping with value transform logic
  end
end

Figure 4

5.  Value transforms

5.1.  General

Value transforms operate on individual attribute values, converting from one type or format to another.

5.2.  Structure

A value transform:

  • Inherits from Lutaml::Value::Transform

  • Defines source and target value types

  • Implements the transform method

  • Implement reverse_transform method if bidirectional

Syntax:

class ValueTransformClass < Lutaml::Value::Transform
  source_value :source_type <1>
  target_value :target_type <2>

  transform do |source_value|
    # transformation logic
  end

  reverse_transform do |target_value|
    # reverse transformation logic
  end
end

Figure 5

source_value

Specifies the source value type. This can be a primitive type or a class that inherits from Lutaml::Value.

target_value

Specifies the target value type. This can be a primitive type or a class that inherits from Lutaml::Value.

transform

Defines the transformation logic.

reverse_transform

Defines the reverse transformation logic for bidirectional transforms.

EXAMPLE

# Transforms a string into a Date model
class DateFormatTransform < Lutaml::Value::Transform
  source_value :string
  target_value :date_with_time

  transform do |source_value|
    Date.parse(source_value)
  end

  reverse_transform do |target_value|
    target_target_value.strftime('%Y-%m-%d')
  end
end

Given:

DateFormatTransform.transform('2021-01-01')
# => #<Date: 2021-01-01 ((2459216j,0s,0n),+0s,2299161j)>

DateFormatTransform.reverse_transform(Date.new(2021, 1, 1))
# => "2021-01-01"

6.  Model transforms

6.1.  General

Model transforms operate on entire model structures, mapping attributes between different model representations.

6.2.  Base requirements

A model transform:

  • Inherits from Lutaml::Model::Transform

  • Specifies source and target models

  • Defines mapping rules within a transform block

  • Declares directionality

Syntax:

class TransformClass < Lutaml::Model::Transform
  source_model :source_model <1>
  target_model :target_model <2>

  transform do
    # mapping rules
  end

  reverse_transform do
    # reverse mapping rules
  end
end

Figure 6

source_model

Specifies the source model class.

target_model

Specifies the target model class.

EXAMPLE

class PublicationTransform < Lutaml::Model::Transform
  source_model Publication
  target_model CatalogEntry

  transform do
    # mapping rules
    map from: 'title', to: 'title'
    map from: 'author', to: 'creator'
  end

  reverse_transform do
    # reverse mapping rules
    map from: 'target.title', to: 'source.title'
    map from: 'target.creator', to: 'source.author'
  end
end

Given:

publication = Publication.new(title: 'The Art of War', author: 'Sun Tzu')
transformed = PublicationTransform.transform(publication)
# => #<CatalogEntry:0x00007f9b1b8b3b10 @title="The Art of War", @creator="Sun Tzu">

publication_transformed = PublicationTransform.reverse_transform(transformed)
# => #<Publication
#      @title="The Art of War",
#      @author="Sun Tzu">

6.3.  Model mapping rules

6.3.1.  Direct attribute mapping

Maps source attributes to target attributes with identical names without value modification.

This type of mapping is bidirectional by default.

Syntax:

map from: 'path_from_source', to: 'path_at_target', directional: :bidirectional # default
# or simply
map from: 'path_from_source', to: 'path_at_target'

Figure 7

The from: and to: parameters are in the LutaML Path syntax.

EXAMPLE 1

In LutaML Path syntax, given a source model of { name("John Doe"), email(" john@example.com") }, the path to the name attribute is name, and the path to the email attribute is email.

EXAMPLE 2 — Direct attribute mapping example

class Publication < Lutaml::Model::Serializable
  attribute :title, :string
  attribute :author, :string
end

class CatalogEntry < Lutaml::Model::Serializable
  attribute :title, :string
  attribute :author, :string
end

class PublicationTransform < Lutaml::Model::Transform
  source_model Publication
  target_model CatalogEntry

  transform do
    map from: 'title', to: 'title'
    map from: 'author', to: 'author'
  end
end

6.3.2.  Attribute renaming

Maps source attributes to differently named target attributes.

This type of mapping is bidirectional by default.

Syntax:

map from: 'old_name', to: 'new_name'

Figure 8

EXAMPLE

class Person < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :year_born, :string
end

class User < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :birth_year, :string
end

class UserTransform < Lutaml::Model::Transform
  source_model Person
  target_model User

  transform do
    map from: 'year_born', to: 'birth_year'
  end
end

6.3.3.  Value transformation mapping

Maps source attributes to target attributes with value transformation.

The directionality of a transformation mapping depends on the directionality of the value transform.

Syntax:

map from: 'attribute', to: 'attribute', transform: TransformClass

Figure 9

EXAMPLE 1

class Person < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :birth_date, :string
end

class User < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :birth_date, :date_with_time
end

# This is a uni-directional transform
class DateFormatTransform < Lutaml::Value::Transform
  source_value :string
  target_value :date_with_time

  transform do |source_value|
    Date.parse(source_value)
  end
end

class UserTransform < Lutaml::Model::Transform
  source_model Person
  target_model User

  transform do
    # This is a uni-directional mapping
    map from: 'birth_date', to: 'birth_date', transform: DateFormatTransform
  end
end

EXAMPLE 2

# This is a bidirectional transform
class DateFormatTransform < Lutaml::Value::Transform
  source_value :string
  target_value :date_with_time

  transform do |source_value|
    Date.parse(source_value)
  end

  reverse_transform do |target_value|
    target_target_value.strftime('%Y-%m-%d')
  end
end

class UserTransform < Lutaml::Model::Transform
  transform do
    # This becomes a bidirectional mapping
    map from: 'birth_date', to: 'birth_date', transform: DateFormatTransform
  end
end

7.  Nested transforms

7.1.  General

Nested transforms handle complex object hierarchies, allowing transformation of nested attributes and objects.

7.2.  Structure

A nested transform:

  • Defines mappings for nested attributes using dot notation

  • Handles collections appropriately

  • Supports value transforms within nested mappings

7.3.  Nested attribute mapping

Maps attributes within nested objects.

The directionality of a nested mapping depends on the directionality of the transform.

Syntax:

# Value to value transformation
map from: 'source_attribute', to: 'destination_attribute'

# Cross-model-value transformation
## Value to model transformation
map from: 'source_model.attribute', to: 'destination_attribute'
## Model to value transformation
map from: 'source_attribute', to: 'destination_model.attribute'

# Model to model transformation
map from: 'source_model.[...]attribute', to: 'destination_model.[...]attribute'

Figure 10

There are three scenarios in transforming nested attributes:

  1. Value to value transformation

  2. Cross model-value transformation

  3. Model to model transformation

7.4.  Value to value transformation

We can apply a value-to-value transformation if both the source and destination attributes are values.

Suppose we have the following source and destination models.

class UnstructuredDateTime < Lutaml::Model::Value
  attribute :value, :string
end

class StructuredDateTime < Lutaml::Model::Value
  attribute :date, :string
  attribute :time, :string
end

class OldDigitalTimepiece < Lutaml::Model::Serializable
  attribute :raw_time, UnstructuredDateTime
end

class NewDigitalTimepiece < Lutaml::Model::Serializable
  attribute :detailed_time, StructuredDateTime
end

Figure 11

First define a value transform:

class DateTimeTransform < Lutaml::Value::Transform
  source_value UnstructuredDateTime
  target_value StructuredDateTime

  transform do |source_value|
    # In ISO 8601-1:2019, a basic date-time string can be "{YYYY}{MM}{DD}T{hh}:{mm}:{ss}"
    date, time = source_value.value.split('T')
    StructuredDateTime.new(date: date, time: time)
  end

  reverse_transform do |target_value|
    UnstructuredDateTime.new(value: "#{target_value.date}T#{target_value.time}")
  end
end

Figure 12

Then define a model transform:

class TimepieceTransform < Lutaml::Model::Transform
  source_model OldDigitalTimepiece
  target_model NewDigitalTimepiece

  transform do
    map from: 'time', to: 'time', transform: DateTimeTransform
  end
end

Figure 13

The resulting transformation is demonstrated with this diagram.

                            transforms via
                            DateTimeTransform
╔════════════════════════════════╗      ╔═══════════════════════════════════╗
║  UnstructuredDateTime          ║      ║  StructuredDateTime               ║
╠════════════════════════════════╣ ───► ╠═══════════════════════════════════╣
║  value: string                 ║      ║  date: string                     ║
║                                ║      ║  time: string                     ║
╚════════════════════════════════╝      ╚═══════════════════════════════════╝
       ▲                                         ▲
       │                                         │
       │has_one                                  │has_one
       │                                         │
╔════════════════════════════════╗      ╔═══════════════════════════════════╗
║ OldDigitalTimepiece            ║      ║ NewDigitalTimepiece               ║
╠════════════════════════════════╣ ───► ╠═══════════════════════════════════╣
║ raw_time: UnstructuredDateTime ║      ║ detailed_time: StructuredDateTime ║
╚════════════════════════════════╝      ╚═══════════════════════════════════╝
                            transforms via
                            TimepieceTransform
────► : Transformation processing

Figure 14

7.5.  Cross model-value transformation

A cross-model-value transformation occurs when one of the source and destination attributes is a model and the other is a value.

Suppose we have a set of deeper source and destination models.

class UnstructuredDateTimeWithOffset < Lutaml::Model::Value
  attribute :value, :string
end

class StructuredDateTimeWithOffset < Lutaml::Model::Model
  attribute :date_time, StructuredDateTime
  attribute :offset, TimeOffset
end

class TimeOffset < Lutaml::Model::Value
  attribute :value, :string
end

class OldDigitalTimepiece < Lutaml::Model::Serializable
  attribute :raw_time, UnstructuredDateTimeWithOffset
end

class NewDigitalTimepiece < Lutaml::Model::Serializable
  attribute :detailed_time, StructuredDateTimeWithOffset
end

Figure 15

Here, the UnstructuredDateTimeWithOffset model is a value, and the StructuredDateTimeWithOffset model is a model.

A cross-model-value transform is defined to transform between UnstructuredDateTimeWithOffset (value) and StructuredDateTimeWithOffset (model).

class DateTimeTransform < Lutaml::Value::Transform
  source_value UnstructuredDateTimeWithOffset
  target_value StructuredDateTimeWithOffset

  # Split the string into date, time, and offset
  transform do |source_value|
    match, date, time, offset = source_value.value.match(/^(.+?)T(.+?)(?:\+(.+))?$/)
    StructuredDateTimeWithOffset.new(
      date_time: StructuredDateTime.new(date: date, time: time),
      offset: TimeOffset.new(value: offset || '00:00')
    )
  end

  reverse_transform do |target_value|
    UnstructuredDateTimeWithOffset.new(
      value:[
        target_value.date_time.date,
        "T",
        target_value.date_time.time,
        "+",
        target_value.zone.offset
      ].join("")
    )
  end
end

Figure 16

Then define a model transform.

class TimepieceTransform < Lutaml::Model::Transform
  source_model OldDigitalTimepiece
  target_model NewDigitalTimepiece

  transform do
    map from: 'raw_time', to: 'detailed_time', transform: DateTimeTransform
  end
end

Figure 17

The resulting transformation is demonstrated with this diagram.

                      ╔═════════════════════════════╗  ╔════════════════════╗
                      ║  StructuredDateTime (Model) ║  ║ TimeOffset (Value) ║
                      ╠═════════════════════════════╣  ╠════════════════════╣
                      ║  date: string               ║  ║ value: string      ║
                      ║  time: string               ║  ║                    ║
                      ╚═════════════════════════════╝  ╚════════════════════╝
                                                 ▲             ▲
                                                 │             │
                                                 │has_one      │has_one
                                                 │             │
                        transforms via           │             │
                        DateTimeTransform        │             │
╔════════════════════════════════╗      ╔═══════════════════════════════════╗
║ UnstructuredDateTimeWithOffset ║      ║ StructuredDateTimeWithOffset      ║
╠════════════════════════════════╣ ───► ╠═══════════════════════════════════╣
║ value: string                  ║      ║ date_time: StructuredDateTime     ║
║                                ║      ║ offset: TimeOffset                ║
╚════════════════════════════════╝      ╚═══════════════════════════════════╝
                    ▲                                         ▲
                    │                                         │
                    │has_one                                  │has_one
                    │                                         │
╔══════════════════════════════════════════╗      ╔═════════════════════════════════════════════╗
║ OldDigitalTimepiece                      ║      ║ NewDigitalTimepiece                         ║
╠══════════════════════════════════════════╣ ───► ╠═════════════════════════════════════════════╣
║ raw_time: UnstructuredDateTimeWithOffset ║      ║ detailed_time: StructuredDateTimeWithOffset ║
╚══════════════════════════════════════════╝      ╚═════════════════════════════════════════════╝
                                        transforms via
                                        TimepieceTransform
────► : Transformation processing

Figure 18

7.6.  Model-to-model transformation

Model-to-model transformations handle mapping between models.

In a model-to-model transformation, a Value Transform may be used to transform the value of an attribute.

When approaching model to model transformation, the mapping can be placed at the desired level of transformation.

  1. Creating the mapping at the most basic level, where either the mapping source or destination is an attribute accessible at root.

    EXAMPLE 1

    In the mapping, a time path is a root attribute, referring to the attribute of {source|target}.time.

  2. Creating the mapping at the model-to-model level, where both source and destination are models, . Given a deep model hierarchy, the transformation can be applied at a shallow level (e.g. close to the underlying value) or at a deeper level (e.g. close to the top-level model, very nested).

    EXAMPLE 2

    A path of clock.time refers to an inner model attribute, referring to the attribute of {source|target}.clock.time.

    A path of computer.clock.time refers to a deeper model attribute, referring to the attribute of {source|target}.computer.clock.time.

The consideration on where the mapping is placed depends on the accessibility, appropriateness and authority of the desired transformation.

A basic level value-to-value or cross-model-value transformation may not be practical when the source and destination models are not managed or owned by the party performing the transformation.

To illustrate such a scenario, we add one more model layer to the same model as in 7.5 to define ComputerClock and WallClock. Then move the transformation logic to the model level.

class UnstructuredDateTimeWithOffset < Lutaml::Model::Value
  attribute :value, :string
end

class StructuredDateTimeWithOffset < Lutaml::Model::Model
  attribute :date_time, StructuredDateTime
  attribute :offset, TimeOffset
end

class TimeOffset < Lutaml::Model::Value
  attribute :value, :string
end

class OldDigitalTimepiece < Lutaml::Model::Serializable
  attribute :raw_time, UnstructuredDateTimeWithOffset
end

class NewDigitalTimepiece < Lutaml::Model::Serializable
  attribute :detailed_time, StructuredDateTimeWithOffset
end

class WallClock < Lutaml::Model::Serializable
  attribute :timepiece, OldDigitalTimepiece
end

class ComputerClock < Lutaml::Model::Serializable
  attribute :timepiece, NewDigitalTimepiece
end

class DateTimeTransform < Lutaml::Value::Transform
  source_value UnstructuredDateTimeWithOffset
  target_value StructuredDateTimeWithOffset

  # Split the string into date, time, and offset
  transform do |source_value|
    match, date, time, offset = source_value.value.match(/^(.+?)T(.+?)(?:\+(.+))?$/)
    StructuredDateTimeWithOffset.new(
      date_time: StructuredDateTime.new(date: date, time: time),
      offset: TimeOffset.new(value: offset || '00:00')
    )
  end

  reverse_transform do |target_value|
    UnstructuredDateTimeWithOffset.new(
      value:[
        target_value.date_time.date,
        "T",
        target_value.date_time.time,
        "+",
        target_value.zone.offset
      ].join("")
    )
  end
end

Figure 19

Now define a model transform between ComputerClock and WallClock.

class ClockTransform < Lutaml::Model::Transform
  source_model WallClock
  target_model ComputerClock

  transform do
    map from: 'timepiece.raw_time', to: 'timepiece.detailed_time', transform: DateTimeTransform
  end
end

Figure 20

Notice that different from the 7.5, the transformation is now applied at the model level, where the mapping is defined at a model-to-model level where both source and destination do not own the attributes involved in transformation.

The resulting transformation is demonstrated with this diagram.

                      ╔═════════════════════════════╗  ╔════════════════════╗
                      ║  StructuredDateTime (Model) ║  ║ TimeOffset (Value) ║
                      ╠═════════════════════════════╣  ╠════════════════════╣
                      ║  date: string               ║  ║ value: string      ║
                      ║  time: string               ║  ║                    ║
                      ╚═════════════════════════════╝  ╚════════════════════╝
                                                 ▲             ▲
                                                 │             │
                                                 │has_one      │has_one
                                                 │             │
                        transforms via           │             │
                        DateTimeTransform        │             │
╔════════════════════════════════╗      ╔═══════════════════════════════════╗
║ UnstructuredDateTimeWithOffset ║      ║ StructuredDateTimeWithOffset      ║
╠════════════════════════════════╣ ───► ╠═══════════════════════════════════╣
║ value: string                  ║      ║ date_time: StructuredDateTime     ║
║                                ║      ║ offset: TimeOffset                ║
╚════════════════════════════════╝      ╚═══════════════════════════════════╝
                    ▲                                         ▲
                    │                                         │
                    │has_one                                  │has_one
                    │                                         │
╔══════════════════════════════════════════╗       ╔═════════════════════════════════════════════╗
║ OldDigitalTimepiece                      ║       ║ NewDigitalTimepiece                         ║
╠══════════════════════════════════════════╣ ─ ─ ► ╠═════════════════════════════════════════════╣
║ raw_time: UnstructuredDateTimeWithOffset ║       ║ detailed_time: StructuredDateTimeWithOffset ║
╚══════════════════════════════════════════╝       ╚═════════════════════════════════════════════╝
                    ▲          Mapping specified via          ▲
                    │          ClockTransform.transform       │
                    │has_one                                  │has_one
                    │                                         │
╔════════════════════════════════╗      ╔════════════════════════════════╗
║ WallClock                      ║      ║ ComputerClock                  ║
╠════════════════════════════════╣ ───► ╠════════════════════════════════╣
║ timepiece: OldDigitalTimepiece ║      ║ timepiece: NewDigitalTimepiece ║
╚════════════════════════════════╝      ╚════════════════════════════════╝
                                transforms via
                                ClockTransform

- - ► : Transformation mapping only
────► : Transformation processing

Figure 21

8.  Collection transforms

8.1.  General

Collection transforms handle mapping between collections of objects.

8.2.  Models to models

The map_each command is used to specify that the source attribute is a collection.

Syntax:

map_each from: 'collection_path', to: 'attribute_or_collection_path', transform: CollectionTransform

Figure 22

EXAMPLE

class Publication < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :authors, Author, collection: true
end

class CatalogEntry < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :contributors, Contributor, collection: true
end

class Author < Lutaml::Model::Serializable
  attribute :name, :string
end

class Contributor < Lutaml::Model::Serializable
  attribute :name, :string
end

# This is a model to model transform
class AuthorTransform < Lutaml::Model::Transform
  source_model Author
  target_model Contributor

  transform do
    map from: 'name', to: 'name'
  end
end

class PublicationTransform < Lutaml::Model::Transform
  source_model Publication
  target_model CatalogEntry

  transform do
    map_each from: 'authors', to: 'contributors', transform: AuthorTransform
  end
end

8.3.  Splitting models into a collection

Collection transforms can also split a single model into multiple entries in a collection. For instance, consider a transform that takes a publication’s authors and converts them into a collection of contributors.

The target_model …​, collection: true syntax is used to specify that the target attribute is a collection.

Syntax:

map_each from: 'attribute', to: 'collection_path', transform: CollectionTransform

Figure 23

EXAMPLE

class PublicationV1 < Lutaml::Model::Serializable
  attribute :title, :string
  attribute :contributor_information, :string
end

class PublicationV2 < Lutaml::Model::Serializable
  attribute :title, :string
  attribute :contributors, Contributor, collection: true
end

class Contributor < Luatml::Model::Serializable
  attribute :name, :string
end

class ContributorTransform < Lutaml::Model::Transform
  source_value :string
  target_model Contributor, collection: true

  transform do |source_value|
    source_value.split(',').map do |name|
      Contributor.new(name: name)
    end
  end
end

class PublicationTransform < Lutaml::Model::Transform
  source_model PublicationV1
  target_model PublicationV2

  transform do
    map from: 'contributor_information', to: 'contributors', transform: ContributorTransform
  end
end

8.4.  Joining a collection into an attribute

Collection transforms can also join a collection of objects into a single attribute in the target model.

Syntax:

map_each from: 'collection_path', to: 'attribute', transform: CollectionTransform

Figure 24

EXAMPLE

class StandardsPublication < Lutaml::Model::Serializable
  attribute :title, :string, collection: true
end

class BibliographyEntry < Lutaml::Model::Serializable
  attribute :title, :string
end

class TitleAggregationTransform < Lutaml::Model::Transform
  source_model :string, collection: true
  target_model :string

  transform do |source_values|
    source_values.join(', ')
  end
end

class StandardsPublicationTransform < Lutaml::Model::Transform
  source_model StandardsPublication
  target_model BibliographyEntry

  transform do
    map_each from: 'title', to: 'title', transform: TitleAggregationTransform
  end
end

Annex A
(normative)

Tutorial: Complex transformation scenario

This tutorial demonstrates a complete transformation scenario using a museum’s art collection as an example.

Consider the following model trees.

The “De Lutam’l Art Museum” has a collection of ceramic pieces managed in a register used for generic art information.

The register has the following fields:

# First model tree
class GenericArtInformation < Lutaml::Model::Serializable
  attribute :title, :string
  attribute :description, :string
  attribute :artist, CreatorInformation
  attribute :creation_date, :string
  attribute :place_of_work, :string
end

class CreatorInformation < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :bio, :string
  attribute :website, :string
  attribute :year_born, :integer
  attribute :year_died, :integer
end

Figure A.1

This is an example of a YAML file that represents the first model tree:



---
- title: "Translucent Vase"
  description: |
    A tall and beautiful translucent vase created in the celadon color.

    Dimensions: 10x10x10 cm
    Fire temperature: 1000°C
    Clay type: Porcelain
  artist:
    name: "Masaaki Shibata"
    bio: |
      Masaaki Shibata is a Japanese ceramic artist.

      Awards: Japan Ceramic Society Award, 2005.

      Skills: Glazing, painting
    website: "https://www.masaakishibata.com"
    year_born: 1947
    year_died: null
  creation_date: "2010-01-01"
  place_of_work: Tokyo, Japan
- title: "Blue and White Bowl"
  description: |
    A blue and white bowl with a floral pattern.

    Dimensions: 20x20x20 cm
    Fire temperature: 1200°C
    Clay type: Stoneware
    Glaze: Blue and white
  artist:
    name: "Lucie Rie"
    bio: |
      Lucie Rie was an Austrian-born British studio potter.

      Awards: Potter's Gold Medal, 1987.

      Skills: Throwing, glazing
    website: "https://www.lucierie.com"
    year_born: 1902
    year_died: 1995
  creation_date: "1970-01-01"
  place_of_work: London, UK
- title: "Ceramic Sculpture"
  description: |
    A ceramic sculpture in form of a golden fish.

    Dimensions: 30x10x20 cm
    Fire temperature: 800°C
    Clay type: Earthenware
    Glaze: Gold
  artist:
    name: "Peter Voulkos"
    bio: |
      Peter Voulkos was an American artist of Greek descent.

      Awards: National Medal of Arts, 2001.

      Skills: Throwing, hand-building, glazing

    website: "https://www.petervoulkos.com"
    year_born: 1924
    year_died: 2002
  creation_date: "1980-01-01"
  place_of_work: Portopolous, Greece

Figure A.2 — In the GenericArtInformation model

The museum wants to transform the generic art information model into a ceramic art information model to better manage the ceramic pieces.

# Second model tree
class CeramicArtInformation < Lutaml::Model::Serializable
  attribute :title, :string
  attribute :description, :string
  attribute :artist, CeramicCreatorInformation
  attribute :creation_date, :date_with_time
  attribute :location, :string
  attribute :dimensions, Dimensions
  attribute :fire_temperature, :integer
  attribute :fire_temperature_unit, :string, values: %w[°C °F]
  attribute :clay_type, :string
  attribute :glaze, :string
end

class Dimensions < Lutaml::Model::Serializable
  attribute :height, :integer
  attribute :width, :integer
  attribute :depth, :integer
end

class CeramicCreatorInformation < Lutaml::Model::Serializable
  attribute :name, :string
  attribute :bio, :string
  attribute :website, :string
  attribute :year_of_birth, :integer
  attribute :year_of_death, :integer
  attribute :techniques, :string, collection: true
  attribute :awards, :string, collection: true
end

Figure A.3

We need to create a Lutaml::Model::Transform class that will transform the first model tree into the second model tree.

Let’s first map the fields and group them according to the level of processing needed.

NOTE  The “source” refes to the GenericArtInformation model, and the “target” refers to the CeramicArtInformation model.

Table A.1 — No processing needed

Source attribute(s)Target attribute(s)Value processing needed
titletitleNone
descriptiondescriptionNone

Table A.2 — Attribute rename

Source attribute(s)Target attribute(s)Value processing needed
place_of_worklocationNone

Table A.3 — Value type conversion

Source attribute(s)Target attribute(s)Value processing needed
creation_datecreation_dateConvert string to date with time

Table A.4 — Processing needed

Source attribute(s)Target attribute(s)Value processing needed
fire_temperaturefire_temperatureExtract from description
fire_techniquefire_techniqueExtract from description
clay_typeclay_typeExtract from description
glazeglazeExtract from description
dimensionsdimensionsExtract specific values and map to Dimensions attributes

Table A.5 — Nested attribute map

Source attribute(s)Target attribute(s)Value processing needed
artist.nameartist.nameNone
artist.bioartist.bioNone
artist.websiteartist.websiteNone

Table A.6 — Nested attribute rename

Source attribute(s)Target attribute(s)Value processing needed
artist.year_bornartist.year_of_birthNone
artist.year_diedartist.year_of_deathNone

Table A.7 — Nested attribute processing

Source attribute(s)Target attribute(s)Value processing needed
artist.bioartist.techniquesExtract from artist bio
artist.bioartist.awardsExtract from artist bio

The following Lutaml::Model::Transform class will transform the first model tree into the second model tree. We build this class incrementally by adding the necessary mappings.

Let’s start with mapping the attributes that do not require any processing.

class CeramicArtInformationTransform < Lutaml::Model::Transform
  source_model GenericArtInformation
  target_model CeramicArtInformation

  transform do
    # Simple mapping
    map from: 'title', to: 'title'
    map from: 'description', to: 'description'
  end
end

Figure A.4

Next, we add the mapping for the attributes that require renaming.

class CeramicArtInformationTransform < Lutaml::Model::Transform
  source_model GenericArtInformation
  target_model CeramicArtInformation

  transform do
    # Simple mapping
    map from: 'title', to: 'title'
    map from: 'description', to: 'description'

    # Rename attributes
    map from: 'place_of_work', to: 'location'
  end
end

Figure A.5

Now let’s add the mapping for the nested attributes.

class CeramicArtInformationTransform < Lutaml::Model::Transform
  source_model GenericArtInformation
  target_model CeramicArtInformation

  transform do
    # Simple mapping
    map from: 'title', to: 'title'
    map from: 'description', to: 'description'

    # Rename attributes
    map from: 'place_of_work', to: 'location'

    # Nested attribute mapping
    map from: 'artist.name', to: 'artist.name'
    map from: 'artist.bio', to: 'artist.bio'
    map from: 'artist.website', to: 'artist.website'

    # Rename nested attributes
    map from: 'artist.year_born', to: 'artist.year_of_birth'
    map from: 'artist.year_died', to: 'artist.year_of_death'
  end
end

Figure A.6

Next, we add the mapping for the attributes that require value type conversion.

There are two ways we can specify a value transform.

  1. By using a Lutaml::Value::Transform class that implements the transform and reverse_transform methods.

  2. By using a block that takes the source value as an argument and returns the transformed value.

In the first manner, we define the DateFormatTransform class that converts a string to a date with time.

class DateFormatTransform < Lutaml::Value::Transform
  source_value :string
  target_value :date_with_time

  transform do |source_value|
    Date.parse(source_value)
  end

  reverse_transform do |target_value|
    target_value.strftime('%Y-%m-%d')
  end
end

Figure A.7

Then the mapping is added to the CeramicArtInformationTransform class like the following.

# Value type conversion
map from: 'creation_date', to: 'creation_date', transform: DateFormatTransform

Figure A.8

In the second manner, we can use a block to specify the transformation.

# Value type conversion
map from: 'creation_date', to: 'creation_date',
  transform: -> { |source_value|
    Date.parse(source_value)
  },
  reverse_transform: -> { |target_value|
    target_value.strftime('%Y-%m-%d')
  }

Figure A.9

The example follows that we follow the first manner.

Next, we add the mapping for the attributes that require processing.

class CeramicArtInformationTransform < Lutaml::Model::Transform
  source_model GenericArtInformation
  target_model CeramicArtInformation

  transform do
    # Simple mapping
    map from: 'title', to: 'title'
    map from: 'description', to: 'description'

    # Rename attributes
    map from: 'place_of_work', to: 'location'

    # Nested attribute mapping
    map from: 'artist.name', to: 'artist.name'
    map from: 'artist.bio', to: 'artist.bio'
    map from: 'artist.website', to: 'artist.website'

    # Rename nested attributes
    map from: 'artist.year_born', to: 'artist.year_of_birth'
    map from: 'artist.year_died', to: 'artist.year_of_death'

    # Value type conversion
    map from: 'creation_date', to: 'creation_date', transform: DateFormatTransform

    # Single direction transform only, because the source information remains
    # unchanged in a reverse migration.
    map from: 'description', to: 'fire_temperature', transform: :extract_fire_temperature
    map from: 'description', to: 'fire_temperature_unit', transform: :extract_fire_temperature_unit

    # Extract the clay type from the description.
    # e.g. "Clay type: Porcelain" => "Porcelain"
    map from: 'description', to: 'clay_type', transform: -> { |description|
      description.match(/Clay type: ([\w\s]+)/)[1]
    }

    # Extract the glaze from the description.
    # e.g. "Glaze: Blue and white" => "Blue and white"
    # Notice that the glaze is (optional), so we use a non-greedy match.
    map from: 'description', to: 'glaze', transform: -> { |description|
      description.match(/Glaze: (.+?)/)[1] rescue nil
    }

    # Use a separate method to extract dimensions from the description.
    # Extract the fire temperature from the description.
    map from: 'description', to: 'fire_temperature', transform: :extract_fire_temperature

    # e.g. "Fire temperature: 1000°C" => 1000
    # NOTE: Fire temperature might not be present.
    def extract_fire_temperature(description)
      description.match(/Fire temperature: (\d+)/)[1]&.to_i
    end

    # Use a separate method to extract dimensions from the description.
    # Extract the temperature unit from the description.
    map from: 'description', to: 'fire_temperature_unit', transform: :extract_fire_temperature_unit

    # e.g. "Fire temperature: 1000°C" => "°C"
    # NOTE: Fire temperature might not be present.
    def extract_fire_temperature_unit(description)
      description.match(/Fire temperature: \d+(°\w+)/)[1]&.to_s
    end

    # Nested attribute with extracted values
    map from: 'artist.bio', to: 'artist.techniques', transform: :extract_techniques
    map from: 'artist.bio', to: 'artist.awards', transform: :extract_awards

    # Extract techniques from the bio text.
    # e.g. "Technique: Pottery" => ["Pottery"]
    def extract_techniques(bio)
      bio.scan(/Technique: ([\w\s]+)/).flatten
    end

    # Extract awards from the bio text.
    # e.g. "Award: Best in Show" => ["Best in Show"]
    def extract_awards(bio)
      bio.scan(/Award: ([\w\s]+)/).flatten
    end
  end
end

Figure A.10

Now we have to create a transformation for the Dimensions attribute.

# Transforms a string into a Dimension model
class DimensionsTransform < Lutaml::Model::Transform
  source_value :string
  target_model :dimensions

  transform do |source_value|
    height, width, depth = source_value.match(/Dimensions: (\d+)x(\d+)x(\d+)/).captures
    target_model.new(
      height: height.to_i,
      width: width.to_i,
      depth: depth.to_i
    )
  end

  reverse_transform do |target_model|
    "#{target_model.height}x#{target_model.width}x#{target_model.depth}"
  end
end

Figure A.11

Then we add the mapping to the CeramicArtInformationTransform class.

# Extract dimensions from the description.
map from: 'description', to: 'dimensions', transform: DimensionsTransform

Figure A.12

Finally, we add the reverse transformation to the CeramicArtInformationTransform class.

class CeramicArtInformationTransform < Lutaml::Model::Transform
  source_model GenericArtInformation
  target_model CeramicArtInformation

  transform do
    # Simple mapping
    map from: 'title', to: 'title'
    map from: 'description', to: 'description'

    # Rename attributes
    map from: 'place_of_work', to: 'location'

    # Nested attribute mapping
    map from: 'artist.name', to: 'artist.name'
    map from: 'artist.bio', to: 'artist.bio'
    map from: 'artist.website', to: 'artist.website'

    # Rename nested attributes
    map from: 'artist.year_born', to: 'artist.year_of_birth'
    map from: 'artist.year_died', to: 'artist.year_of_death'

    # Value type conversion
    map from: 'creation_date', to: 'creation_date', transform: DateFormatTransform

    # Single direction transform only, because the source information remains
    # unchanged in a reverse migration.
    map from: 'description', to: 'fire_temperature', transform: :extract_fire_temperature
    map from: 'description', to: 'fire_temperature_unit', transform: :extract_fire_temperature_unit

    # Extract the clay type from the description.
    # e.g. "Clay type: Porcelain" => "Porcelain"
    map from: 'description', to: 'clay_type', transform: -> { |description|
      description.match(/Clay type: ([\w\s]+)/)[1]
    }

    # Extract the glaze from the description.
    # e.g. "Glaze: Blue and white" => "Blue and white"
    # Notice that the glaze is (optional), so we use a non-greedy match.
    map from: 'description', to: 'glaze', transform: -> { |description|
      description.match(/Glaze: (.+?)/)[1] rescue nil
    }

    # Use a separate method to extract dimensions from the description.
    # Extract the fire temperature from the description.
    map from: 'description', to: 'fire_temperature', transform: :extract_fire_temperature

    # e.g. "Fire temperature: 1000°C" => 1000
    # NOTE: Fire temperature might not be present.
    def extract_fire_temperature(description)
      description.match(/Fire temperature: (\d+)/)[1]&.to_i
    end

    # Use a separate method to extract dimensions from the description.
    # Extract the temperature unit from the description.
    map from: 'description', to: 'fire_temperature_unit', transform: :extract_fire_temperature_unit

    # e.g. "Fire temperature: 1000°C" => "°C"
    # NOTE: Fire temperature might not be present.
    def extract_fire_temperature_unit(description)
      description.match(/Fire temperature: \d+(°\w+)/)[1]&.to_s
    end

    # Nested attribute with extracted values
    map from: 'artist.bio', to: 'artist.techniques', transform: :extract_techniques
    map from: 'artist.bio', to: 'artist.awards', transform: :extract_awards

    # Extract techniques from the bio text.
    # e.g. "Technique: Pottery" => ["Pottery"]
    def extract_techniques(bio)
      bio.scan(/Technique: ([\w\s]+)/).flatten
    end

    # Extract awards from the bio text.
    # e.g. "Award: Best in Show" => ["Best in Show"]
    def extract_awards(bio)
      bio.scan(/Award: ([\w\s]+)/).flatten
    end

    # Extract dimensions from the description.
    map from: 'description', to: 'dimensions', transform:
   
DimensionsTransform
  end
end

# Transforms a string into a Dimension model
class DimensionsTransform < Lutaml::Model::Transform
  source_value :string
  target_model Dimensions

  transform do |source_value|
    height, width, depth = source_value.match(/Dimensions: (\d+)x(\d+)x(\d+)/).captures
    target_model.new(
      height: height.to_i,
      width: width.to_i,
      depth: depth.to_i
    )
  end

  reverse_transform do |target_model|
    "#{target_model.height}x#{target_model.width}x#{target_model.depth}"
  end
end

# Transforms a string into a Date model
class DateFormatTransform < Lutaml::Value::Transform
  source_value :string
  target_value :date_with_time

  transform do |source_value|
    Date.parse(source_value)
  end

  reverse_transform do |target_value|
    target_value.strftime('%Y-%m-%d')
  end
end

Figure A.13

The transformation is now complete.

We can now use the CeramicArtInformationTransform class to transform the data from the first model tree to the second model tree.

# Load the data from the YAML file
data = YAML.load_file('generic_art_information.yaml')

# Load the generic art information
generic_art_info = GenericArtInformation.from_yaml(data)

# Transform the data
transformed_data = CeramicArtInformationTransform.transform(generic_art_info)

transformed_data.first.class
# => CeramicArtInformation

# Save the transformed data to a YAML file
File.write('ceramic_art_information.yaml', transformed_data.to_yaml)

Figure A.14

The transformed data looks like this.



---
- title: "Translucent Vase"
  description: |
    A tall and beautiful translucent vase created in the celadon color.

    Dimensions: 10x10x10 cm
    Fire temperature: 1000°C
    Clay type: Porcelain
  artist:
    name: "Masaaki Shibata"
    bio: |
      Masaaki Shibata is a Japanese ceramic artist.

      Awards: Japan Ceramic Society Award, 2005.

      Skills: Glazing, painting
    website: "https://www.masaakishibata.com"
    year_of_birth: 1947
    year_of_death: null
    techniques:
      - "Glazing"
      - "Painting"
    awards:
      - "Japan Ceramic Society Award, 2005"
  creation_date: "2010-01-01"
  location: Tokyo, Japan
  dimensions:
    height: 10
    width: 10
    depth: 10
  fire_temperature: 1000
  fire_temperature_unit: "°C"
  clay_type: "Porcelain"
  glaze: null
- title: "Blue and White Bowl"
  description: |
    A blue and white bowl with a floral pattern.

    Dimensions: 20x20x20 cm
    Fire temperature: 1200°C
    Clay type: Stoneware
    Glaze: Blue and white
  artist:
    name: "Lucie Rie"
    bio: |
      Lucie Rie was an Austrian-born British studio potter.

      Awards: Potter's Gold Medal, 1987.

      Skills: Throwing, glazing
    website: "https://www.lucierie.com"
    year_of_birth: 1902
    year_of_death: 1995
    techniques:
      - "Throwing"
      - "Glazing"
    awards:
      - "Potter's Gold Medal, 1987"
  creation_date: "1970-01-01"
  location: London, UK
  dimensions:
    height: 20
    width: 20
    depth: 20
  fire_temperature: 1200
  fire_temperature_unit: "°C"
  clay_type: "Stoneware"
  glaze: "Blue and white"
- title: "Ceramic Sculpture"
  description: |
    A ceramic sculpture in form of a golden fish.

    Dimensions: 30x10x20 cm
    Fire temperature: 800°C
    Clay type: Earthenware
    Glaze: Gold
  artist:
    name: "Peter Voulkos"
    bio: |
      Peter Voulkos was an American artist of Greek descent.

      Awards: National Medal of Arts, 2001.

      Skills: Throwing, hand-building, glazing

    website: "https://www.petervoulkos.com"
    year_of_birth: 1924
    year_of_death: 2002
    techniques:
      - "Throwing"
      - "Hand-building"
      - "Glazing"
    awards:
      - "National Medal of Arts, 2001"
  creation_date: "1980-01-01"
  location: Portopolous, Greece
  dimensions:
    height: 30
    width: 10
    depth: 20
  fire_temperature: 800
  fire_temperature_unit: "°C"
  clay_type: "Earthenware"
  glaze: "Gold"

Figure A.15 — Data instances in the CeramicArtInformation model