= LutaML Ruby modeller https://github.com/lutaml/lutaml-model[image:https://img.shields.io/github/stars/lutaml/lutaml-model.svg?style=social[GitHub Stars]] https://github.com/lutaml/lutaml-model[image:https://img.shields.io/github/forks/lutaml/lutaml-model.svg?style=social[GitHub Forks]] image:https://img.shields.io/github/license/lutaml/lutaml-model.svg[License] image:https://img.shields.io/github/actions/workflow/status/lutaml/lutaml-model/test.yml?branch=main[Build Status] image:https://img.shields.io/gem/v/lutaml-model.svg[RubyGems Version] == Purpose Lutaml::Model is a lightweight library for serializing and deserializing Ruby objects to and from various formats such as JSON, XML, YAML, and TOML. It uses an adapter pattern to support multiple libraries for each format, providing flexibility and extensibility for your data modeling needs. NOTE: Lutaml::Model is designed to be mostly compatible with the data modeling API of https://www.shalerb.org[Shale], an impressive Ruby data modeller. Lutaml::Model is meant to address advanced needs not currently addressed by Shale. NOTE: Instructions on how to migrate from Shale to Lutaml::Model are provided in <>. == Data modeling in a nutshell Data modeling is the process of creating a data model for the data to be stored in a database or used in an application. It helps in defining the structure, relationships, and constraints of the data, making it easier to manage and use. Lutaml::Model simplifies data modeling in Ruby by allowing you to define models with attributes and serialize/deserialize them to/from various serialization formats seamlessly. == Features * Define models with attributes and types * Serialize and deserialize models to/from JSON, XML, YAML, and TOML * Support for multiple serialization libraries (e.g., `toml-rb`, `tomlib`) * Configurable adapters for different serialization formats * Support for collections and default values * Custom serialization/deserialization methods * XML namespaces and mappings == Installation Add this line to your application's Gemfile: [source,ruby] ---- gem 'lutaml-model' ---- And then execute: [source,shell] ---- bundle install ---- Or install it yourself as: [source,shell] ---- gem install lutaml-model ---- == Data model class === Definition ==== General There are two ways to define a data model in Lutaml::Model: * Inheriting from the `Lutaml::Model::Serializable` class * Including the `Lutaml::Model::Serialize` module [[define-through-inheritance]] ==== 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. [source,ruby] ---- require 'lutaml/model' class Kiln < Lutaml::Model::Serializable attribute :brand, :string attribute :capacity, :integer attribute :temperature, :integer end ---- [[define-through-inclusion]] ==== Definition 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. [source,ruby] ---- require 'lutaml/model' class Kiln < SomeSuperClass include Lutaml::Model::Serialize attribute :brand, :string attribute :capacity, :integer attribute :temperature, :integer end ---- === Comparison A `Serialize` / `Serializable` object can be compared with another object of the same class using the `==` operator. This is implemented through the `ComparableModel` module. 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. NOTE: Two `Serialize` objects will have the same `hash` value if they have the same class and all their attributes are equal. [source,ruby] ---- > 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 ---- == Defining attributes === Supported attribute value types ==== General types Lutaml::Model supports the following attribute types, they can be referred by a string, a symbol, or their class constant. Syntax: [source,ruby] ---- attribute :name_of_attribute, {symbol | string | class} ---- |=== | String | Symbol | Class name | Actual value class | `String` | `:string` | `Lutaml::Model::Type::String` | `::String` | `Integer` | `:integer` | `Lutaml::Model::Type::Integer` | `::Integer` | `Float` | `:float` | `Lutaml::Model::Type::Float` | `::Float` | `Date` | `:date` | `Lutaml::Model::Type::Date` | `::Date` | `Time` | `:time` | `Lutaml::Model::Type::Time` | `::Time` | `DateTime` | `:date_time` | `Lutaml::Model::Type::DateTime` | `::DateTime` | `TimeWithoutDate` | `:time_without_date` | `Lutaml::Model::Type::TimeWithoutDate` | `::Time` | `Boolean` | `:boolean` | `Lutaml::Model::Type::Boolean` | `Boolean` | `Decimal` (optional) | `:decimal` | `Lutaml::Model::Type::Decimal` | `::BigDecimal` | `Hash` | `:hash` | `Lutaml::Model::Type::Hash` | `::Hash` |=== .Defining attributes with supported types via symbol, string and class [example] ==== [source,ruby] ---- class Studio < Lutaml::Model::Serializable # The following are equivalent attribute :location, :string attribute :potter, "String" attribute :kiln, :string end ---- [source,ruby] ---- > s = Studio.new(location: 'London', potter: 'John Doe', kiln: 'Kiln 1') > # > s.location > # "London" > s.potter > # "John Doe" > s.kiln > # "Kiln 1" ---- ==== ==== (optional) Decimal type The `BigDecimal` class is no longer part of the standard Ruby library from Ruby 3.4 onwards, hence the `Decimal` type is only enabled when the `bigdecimal` library is loaded. This means that the following code needs to be run before using (and parsing) the `Decimal` type: [source,ruby] ---- require 'bigdecimal' ---- If the `bigdecimal` library is not loaded, usage of the `Decimal` type will raise a `Lutaml::Model::TypeNotSupportedError`. === Attribute as a collection Define attributes as collections (arrays or hashes) to store multiple values using the `collection` option. `collection` can be set to: `true`::: The attribute contains an unbounded collection of objects of the declared class. `{min}..{max}`::: The attribute contains a collection of objects of the declared class with a count within the specified range. If the number of objects is out of this numbered range, `CollectionCountOutOfRangeError` will be raised. + [example] ==== When set to `0..1`, it means that the attribute is optional, it could be empty or contain one object of the declared class. ==== + [example] ==== When set to `1..` (equivalent to `1..Infinity`), it means that the attribute must contain at least one object of the declared class and can contain any number of objects. ==== + [example] ==== When set to 5..10` means that there is a minimum of 5 and a maximum of 10 objects of the declared class. If the count of values for the attribute is less then 5 or greater then 10, the `CollectionCountOutOfRangeError` will be raised. ==== Syntax: [source,ruby] ---- attribute :name_of_attribute, Type, collection: true attribute :name_of_attribute, Type, collection: {min}..{max} attribute :name_of_attribute, Type, collection: {min}.. ---- .Using the `collection` option to define a collection attribute [example] ==== [source,ruby] ---- class Studio < Lutaml::Model::Serializable attribute :location, :string attribute :potters, :string, collection: true attribute :address, :string, collection: 1..2 attribute :hobbies, :string, collection: 0.. end ---- [source,ruby] ---- > Studio.new > # address count is `0`, must be between 1 and 2 (Lutaml::Model::CollectionCountOutOfRangeError) > Studio.new({ address: ["address 1", "address 2", "address 3"] }) > # address count is `3`, must be between 1 and 2 (Lutaml::Model::CollectionCountOutOfRangeError) > Studio.new({ address: ["address 1"] }).potters > # [] > Studio.new({ address: ["address 1"] }).address > # ["address 1"] > Studio.new(address: ["address 1"], potters: ['John Doe', 'Jane Doe']).potters > # ['John Doe', 'Jane Doe'] ---- ==== [[attribute-enumeration]] === Attribute as an enumeration An attribute can be defined as an enumeration by using the `values` directive. The `values` directive is used to define acceptable values in an attribute. If any other value is given, a `Lutaml::Model::InvalidValueError` will be raised. Syntax: [source,ruby] ---- attribute :name_of_attribute, Type, values: [value1, value2, ...] ---- The values set inside the `values:` option can be of any type, but they must match the type of the attribute. The values are compared using the `==` operator, so the type must implement the `==` method. .Using the `values` directive to define acceptable values for an attribute (basic types) [example] ==== [source,ruby] ---- class GlazeTechnique < Lutaml::Model::Serializable attribute :name, :string, values: ["Celadon", "Raku", "Majolica"] end ---- [source,ruby] ---- > GlazeTechnique.new(name: "Celadon").name > # "Celadon" > GlazeTechnique.new(name: "Raku").name > # "Raku" > GlazeTechnique.new(name: "Majolica").name > # "Majolica" > GlazeTechnique.new(name: "Earthenware").name > # Lutaml::Model::InvalidValueError: Invalid value for attribute 'name' ---- ==== The values can be Serialize objects, which are compared using the `==` and the `hash` methods through the Lutaml::Model::ComparableModel module. .Using the `values` directive to define acceptable values for an attribute (Serializable objects) [example] ==== [source,ruby] ---- class Ceramic < Lutaml::Model::Serializable attribute :type, :string attribute :firing_temperature, :integer end class CeramicCollection < Lutaml::Model::Serializable attribute :featured_piece, Ceramic, values: [ Ceramic.new(type: "Porcelain", firing_temperature: 1300), Ceramic.new(type: "Stoneware", firing_temperature: 1200), Ceramic.new(type: "Earthenware", firing_temperature: 1000), ] end ---- [source,ruby] ---- > CeramicCollection.new(featured_piece: Ceramic.new(type: "Porcelain", firing_temperature: 1300)).featured_piece > # Ceramic:0x0000000104ac7240 @type="Porcelain", @firing_temperature=1300 > CeramicCollection.new(featured_piece: Ceramic.new(type: "Bone China", firing_temperature: 1300)).featured_piece > # Lutaml::Model::InvalidValueError: Invalid value for attribute 'featured_piece' ---- ==== Serialize provides a `validate` method that checks if all its attributes have valid values. This is necessary for the case when a value is valid at the component level, but not accepted at the aggregation level. If a change has been made at the component level (a nested attribute has changed), the aggregation level needs to call the `validate` method to verify acceptance of the newly updated component. .Using the `validate` method to check if all attributes have valid values [example] ==== [source,ruby] ---- > collection = CeramicCollection.new(featured_piece: Ceramic.new(type: "Porcelain", firing_temperature: 1300)) > collection.featured_piece.firing_temperature = 1400 > # No error raised in changed nested attribute > collection.validate > # Lutaml::Model::InvalidValueError: Invalid value for attribute 'featured_piece' ---- ==== === Attribute value default and rendering defaults Specify default values for attributes using the `default` option. The `default` option can be set to a value or a lambda that returns a value. Syntax: [source,ruby] ---- attribute :name_of_attribute, Type, default: -> { value } ---- .Using the `default` option to set a default value for an attribute [example] ==== [source,ruby] ---- class Glaze < Lutaml::Model::Serializable attribute :color, :string, default: -> { 'Clear' } attribute :temperature, :integer, default: -> { 1050 } end ---- [source,ruby] ---- > Glaze.new.color > # "Clear" > Glaze.new.temperature > # 1050 ---- ==== The "default behavior" (pun intended) is to not render a default value if the current value is the same as the default value. In certain cases, it is necessary to render the default value even if the current value is the same as the default value. This can be achieved by setting the `render_default` option to `true`. Syntax: [source,ruby] ---- attribute :name_of_attribute, Type, default: -> { value }, render_default: true ---- .Using the `render_default` option to force encoding the default value [example] ==== [source,ruby] ---- class Glaze < Lutaml::Model::Serializable attribute :color, :string, default: -> { 'Clear' } attribute :opacity, :string, default: -> { 'Opaque' } attribute :temperature, :integer, default: -> { 1050 } attribute :firing_time, :integer, default: -> { 60 } xml do root "glaze" map_element 'color', to: :color map_element 'opacity', to: :opacity, render_default: true map_attribute 'temperature', to: :temperature map_attribute 'firingTime', to: :firing_time, render_default: true end json do map 'color', to: :color map 'opacity', to: :opacity, render_default: true map 'temperature', to: :temperature map 'firingTime', to: :firing_time, render_default: true end end ---- .Attributes with `render_default: true` are rendered when the value is identical to the default [example] ==== [source,ruby] ---- > glaze_new = Glaze.new > puts glaze_new.to_xml # # Opaque # > puts glaze_new.to_json # {"firingTime":60,"opacity":"Opaque"} ---- ==== .Attributes with `render_default: true` with non-default values are rendered [example] ==== [source,ruby] ---- > glaze = Glaze.new(color: 'Celadon', opacity: 'Semitransparent', temperature: 1300, firing_time: 90) > puts glaze.to_xml # # Semitransparent # > puts glaze.to_json # {"color":"Celadon","temperature":1300,"firingTime":90,"opacity":"Semitransparent"} ---- ==== === Attribute as raw string An attribute can be set to read the value as raw string for XML, by using the `raw: true` option. Syntax: [source,ruby] ---- attribute :name_of_attribute, :string, raw: true ---- .Using the `raw` option to read raw value for an XML attribute [example] ==== [source,ruby] ---- class Person < Lutaml::Model::Serializable attribute :name, :string attribute :description, :string, raw: true end ---- For the following xml [source,xml] ---- John Doe A fictional person commonly used as a placeholder name. ---- [source,ruby] ---- > Person.from_xml(xml) > # ---- ==== == Serialization model mappings === General Lutaml::Model allows you to translate a data model into serialization models of various serialization formats including XML, JSON, YAML, and TOML. Depending on the serialization format, different methods are supported for defining serialization and deserialization mappings. Serialization model mappings are defined under the `xml`, `json`, `yaml`, and `toml` blocks. .Using the `xml`, `json`, `yaml`, and `toml` blocks to define serialization mappings [source,ruby] ---- class Example < Lutaml::Model::Serializable xml do # ... end json do # ... end yaml do # ... end toml do # ... end end ---- === XML ==== Setting root element name The `root` method sets the root element tag name of the XML document. If `root` is not given, then the snake-cased class name will be used as the root. [example] Sets the tag name for `` in XML `...`. Syntax: [source,ruby] ---- xml do root 'xml_element_name' end ---- .Setting the root element name to `example` [example] ==== [source,ruby] ---- class Example < Lutaml::Model::Serializable xml do root 'example' end end ---- [source,ruby] ---- > Example.new.to_xml > # ---- ==== ==== Mapping elements The `map_element` method maps an XML element to a data model attribute. [example] To handle the `` tag in `John Doe`. The value will be set to `John Doe`. Syntax: [source,ruby] ---- xml do map_element 'xml_element_name', to: :name_of_attribute end ---- .Mapping the `name` tag to the `name` attribute [example] ==== [source,ruby] ---- class Example < Lutaml::Model::Serializable attribute :name, :string xml do root 'example' map_element 'name', to: :name end end ---- [source,xml] ---- John Doe ---- [source,ruby] ---- > Example.from_xml(xml) > # > Example.new(name: "John Doe").to_xml > #John Doe ---- ==== If an element is mapped to a model object with the XML `root` tag name set, the mapped tag name will be used as the root name, overriding the root name. .The mapped tag name is used as the root name [example] ==== [source,ruby] ---- class RecordDate < Lutaml::Model::Serializable attribute :content, :string xml do root "recordDate" map_content to: :content end end class OriginInfo < Lutaml::Model::Serializable attribute :date_issued, RecordDate, collection: true xml do root "originInfo" map_element "dateIssued", to: :date_issued end end ---- [source,ruby] ---- > RecordDate.new(date: "2021-01-01").to_xml > #2021-01-01 > OriginInfo.new(date_issued: [RecordDate.new(date: "2021-01-01")]).to_xml > #2021-01-01 ---- ==== ==== Mapping attributes The `map_attribute` method maps an XML attribute to a data model attribute. Syntax: [source,ruby] ---- xml do map_attribute 'xml_attribute_name', to: :name_of_attribute end ---- .Using `map_attribute` to map the `value` attribute [example] ==== The following class will parse the XML snippet below: [source,ruby] ---- class Example < Lutaml::Model::Serializable attribute :value, :integer xml do root 'example' map_attribute 'value', to: :value end end ---- [source,xml] ---- John Doe ---- [source,ruby] ---- > Example.from_xml(xml) > # > Example.new(value: 12).to_xml > # ---- The `map_attribute` method does not inherit the root element's namespace. To specify a namespace for an attribute, please explicitly declare the *namespace* and *prefix* in the `map_attribute` method. [example] ==== The following class will parse the XML snippet below: [source,ruby] ---- class Attribute < Lutaml::Model::Serializable attribute :value, :integer xml do root 'example' map_attribute 'value', to: :value, namespace: "http://www.tech.co/XMI", prefix: "xl" end end ---- [source,xml] ---- ---- [source,ruby] ---- > Attribute.from_xml(xml) > # > Attribute.new(value: 20).to_xml > # ---- ==== ==== Mapping content Content represents the text inside an XML element, inclusive of whitespace. The `map_content` method maps an XML element's content to a data model attribute. Syntax: [source,ruby] ---- xml do map_content to: :name_of_attribute end ---- .Using `map_content` to map content of the `description` tag [example] ==== The following class will parse the XML snippet below: [source,ruby] ---- class Example < Lutaml::Model::Serializable attribute :description, :string xml do root 'example' map_content to: :description end end ---- [source,xml] ---- John Doe is my moniker. ---- [source,ruby] ---- > Example.from_xml(xml) > # > Example.new(description: "John Doe is my moniker.").to_xml > #John Doe is my moniker. ---- ==== ==== Example for mapping [example] ==== The following class will parse the XML snippet below: [source,ruby] ---- class Example < Lutaml::Model::Serializable attribute :name, :string attribute :description, :string attribute :value, :integer xml do root 'example' map_element 'name', to: :name map_attribute 'value', to: :value map_content to: :description end end ---- [source,xml] ---- John Doe is my moniker. ---- [source,ruby] ---- > Example.from_xml(xml) > # > Example.new(name: "John Doe", description: " is my moniker.", value: 12).to_xml > #John Doe is my moniker. ---- ==== ==== Namespaces [[root-namespace]] ===== Namespace at root The `namespace` method in the `xml` block sets the namespace for the root element. Syntax: .Setting default namespace at the root element [source,ruby] ---- xml do namespace 'http://example.com/namespace' end ---- .Setting a prefixed namespace at the root element [source,ruby] ---- xml do namespace 'http://example.com/namespace', 'prefix' end ---- .Using the `namespace` method to set the namespace for the root element [example] ==== [source,ruby] ---- class Ceramic < Lutaml::Model::Serializable attribute :type, :string attribute :glaze, :string xml do root 'Ceramic' namespace 'http://example.com/ceramic' map_element 'Type', to: :type map_element 'Glaze', to: :glaze end end ---- [source,xml] ---- PorcelainClear ---- [source,ruby] ---- > Ceramic.from_xml(xml_file) > # > Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml > #PorcelainClear ---- ==== .Using the `namespace` method to set a prefixed namespace for the root element [example] ==== [source,ruby] ---- class Ceramic < Lutaml::Model::Serializable attribute :type, :string attribute :glaze, :string xml do root 'Ceramic' namespace 'http://example.com/ceramic', 'cer' map_element 'Type', to: :type map_element 'Glaze', to: :glaze end end ---- [source,xml] ---- PorcelainClear ---- [source,ruby] ---- > Ceramic.from_xml(xml_file) > # > Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml > #PorcelainClear ---- ==== ===== Namespace on attribute If the namespace is defined on a model attribute that already has a namespace, the mapped namespace will be given priority over the one defined in the class. Syntax: [source,ruby] ---- xml do map_element 'xml_element_name', to: :name_of_attribute, namespace: 'http://example.com/namespace', prefix: 'prefix' end ---- `namespace`:: The XML namespace used by this element `prefix`:: The XML namespace prefix used by this element (optional) .Using the `namespace` option to set the namespace for an element [example] ==== In this example, `glz` will be used for `Glaze` if it is added inside the `Ceramic` class, and `glaze` will be used otherwise. [source,ruby] ---- class Ceramic < Lutaml::Model::Serializable attribute :type, :string attribute :glaze, Glaze xml do root 'Ceramic' namespace 'http://example.com/ceramic' map_element 'Type', to: :type map_element 'Glaze', to: :glaze, namespace: 'http://example.com/glaze', prefix: "glz" end end class Glaze < Lutaml::Model::Serializable attribute :color, :string attribute :temperature, :integer xml do root 'Glaze' namespace 'http://example.com/old_glaze', 'glaze' map_element 'color', to: :color map_element 'temperature', to: :temperature end end ---- [source,xml] ---- Porcelain Clear 1050 ---- [source,ruby] ---- > # Using the original Glaze class namespace > Glaze.new(color: "Clear", temperature: 1050).to_xml > #Clear1050 > # Using the Ceramic class namespace for Glaze > Ceramic.from_xml(xml_file) > #> > Ceramic.new(type: "Porcelain", glaze: Glaze.new(color: "Clear", temperature: 1050)).to_xml > #PorcelainClear1050 ---- ==== [[namespace-inherit]] ===== Namespace with `inherit` option The `inherit` option is used at the element level to inherit the namespace from the root element. Syntax: [source,ruby] ---- xml do map_element 'xml_element_name', to: :name_of_attribute, namespace: :inherit end ---- .Using the `inherit` option to inherit the namespace from the root element [example] ==== In this example, the `Type` element will inherit the namespace from the root. [source,ruby] ---- class Ceramic < Lutaml::Model::Serializable attribute :type, :string attribute :glaze, :string attribute :color, :string xml do root 'Ceramic' namespace 'http://example.com/ceramic', 'cera' map_element 'Type', to: :type, namespace: :inherit map_element 'Glaze', to: :glaze map_attribute 'color', to: :color, namespace: 'http://example.com/color', prefix: 'clr' end end ---- [source,xml] ---- Porcelain Clear ---- [source,ruby] ---- > Ceramic.from_xml(xml_file) > # > Ceramic.new(type: "Porcelain", glaze: "Clear", color: "navy-blue").to_xml > # # Porcelain # Clear # ---- ==== [[mixed-content]] ==== Mixed content In XML there can be tags that contain content mixed with other tags and where whitespace is significant, such as to represent rich text. [example] ==== [source,xml] ----

My name is John Doe, and I'm 28 years old

---- ==== To map this to Lutaml::Model we can use the `mixed` option in either way: * when defining the model; * when referencing the model. NOTE: This feature is not supported by Shale. To specify mixed content, the `mixed: true` option needs to be set at the `xml` block's `root` method. Syntax: [source,ruby] ---- xml do root 'xml_element_name', mixed: true end ---- .Applying `mixed` to treat root as mixed content [example] ==== [source,ruby] ---- class Paragraph < Lutaml::Model::Serializable attribute :bold, :string, collection: true # allows multiple bold tags attribute :italic, :string xml do root 'p', mixed: true map_element 'bold', to: :bold map_element 'i', to: :italic end end ---- [source,ruby] ---- > Paragraph.from_xml("

My name is John Doe, and I'm 28 years old

") > # > Paragraph.new(bold: "John Doe", italic: "28").to_xml > #

My name is John Doe, and I'm 28 years old

---- ==== // TODO: How to create mixed content from `#new`? [[xml-schema-location]] ==== Automatic support of `xsi:schemaLocation` The https://www.w3.org/TR/xmlschema-1/#xsi_schemaLocation[W3C "XMLSchema-instance"] namespace describes a number of attributes that can be used to control the behavior of XML processors. One of these attributes is `xsi:schemaLocation`. The `xsi:schemaLocation` attribute locates schemas for elements and attributes that are in a specified namespace. Its value consists of pairs of a namespace URI followed by a relative or absolute URL where the schema for that namespace can be found. Usage of `xsi:schemaLocation` in an XML element depends on the declaration of the XML namespace of `xsi`, i.e. `xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"`. Without this namespace LutaML will not be able to serialize the `xsi:schemaLocation` attribute. NOTE: It is most commonly attached to the root element but can appear further down the tree. The following snippet shows how `xsi:schemaLocation` is used in an XML document: [source,xml] ---- Porcelain Clear ---- LutaML::Model supports the `xsi:schemaLocation` attribute in all XML serializations by default, through the `schema_location` attribute on the model instance object. .Retrieving and setting the `xsi:schemaLocation` attribute in XML serialization [example] ==== In this example, the `xsi:schemaLocation` attribute will be automatically supplied without the explicit need to define in the model, and allows for round-trip serialization. [source,ruby] ---- class Ceramic < Lutaml::Model::Serializable attribute :type, :string attribute :glaze, :string attribute :color, :string xml do root 'Ceramic' namespace 'http://example.com/ceramic', 'cera' map_element 'Type', to: :type, namespace: :inherit map_element 'Glaze', to: :glaze map_attribute 'color', to: :color, namespace: 'http://example.com/color', prefix: 'clr' end end xml_content = <<~HERE Porcelain Clear HERE ---- [source,ruby] ---- > c = Ceramic.from_xml(xml_content) => # schema_loc = c.schema_location # schema_loc => #, #]> > new_c = Ceramic.new(type: "Porcelain", glaze: "Clear", color: "navy-blue", schema_location: schema_loc).to_xml > puts new_c # # Porcelain # Clear # ---- ==== NOTE: For details on `xsi:schemaLocation`, please refer to the https://www.w3.org/TR/xmlschema-1/#xsi_schemaLocation[W3C XML standard]. === Key value data models ==== General Key-value data models like JSON, YAML, and TOML all share a similar structure where data is stored as key-value pairs. Lutaml::Model works with these formats in a similar way. ==== Mapping The `map` method is used to define key-value mappings. Syntax: [source,ruby] ---- json | yaml | toml do map 'key_value_model_attribute_name', to: :name_of_attribute end ---- .Using the `map` method to define key-value mappings [example] ==== [source,ruby] ---- class Example < Lutaml::Model::Serializable attribute :name, :string attribute :value, :integer json do map 'name', to: :name map 'value', to: :value end yaml do map 'name', to: :name map 'value', to: :value end toml do map 'name', to: :name map 'value', to: :value end end ---- [source,json] ---- { "name": "John Doe", "value": 28 } ---- [source,ruby] ---- > Example.from_json(json) > # > Example.new(name: "John Doe", value: 28).to_json > #{"name"=>"John Doe", "value"=>28} ---- ==== ==== Nested attribute mappings The `map` method can also be used to map nested key-value data models by referring to a Lutaml::Model class as an attribute class. [example] ==== [source,ruby] ---- class Glaze < Lutaml::Model::Serializable attribute :color, :string attribute :temperature, :integer json do map 'color', to: :color map 'temperature', to: :temperature end end class Ceramic < Lutaml::Model::Serializable attribute :type, :string attribute :glaze, Glaze json do map 'type', to: :type map 'glaze', to: :glaze end end ---- [source,json] ---- { "type": "Porcelain", "glaze": { "color": "Clear", "temperature": 1050 } } ---- [source,ruby] ---- > Ceramic.from_json(json) > #> > Ceramic.new(type: "Porcelain", glaze: Glaze.new(color: "Clear", temperature: 1050)).to_json > #{"type"=>"Porcelain", "glaze"=>{"color"=>"Clear", "temperature"=>1050}} ---- ==== [[separate-serialization-model]] === Separate serialization model The `Serialize` module can be used to define only serialization mappings for a separately defined model (a Ruby class). Syntax: [source,ruby] ---- class Foo < Lutaml::Model::Serializable model {DataModelClass} # ... end ---- [example] .Using the `model` method to define serialization mappings for a separate model ==== [source,ruby] ---- class Ceramic attr_accessor :type, :glaze def name "#{type} with #{glaze}" end end class CeramicSerialization < Lutaml::Model::Serializable model Ceramic xml do map_element 'type', to: :type map_element 'glaze', to: :glaze end end ---- [source,ruby] ---- > Ceramic.new(type: "Porcelain", glaze: "Clear").name > # "Porcelain with Clear" > CeramicSerialization.from_xml(xml) > # > Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml > #PorcelainClear ---- ==== === Rendering empty attributes and collections By default, empty attributes and collections are not rendered in the output. To render empty attributes and collections, use the `render_nil` option. Syntax: [source,ruby] ---- xml do map_element 'key_value_model_attribute_name', to: :name_of_attribute, render_nil: true end ---- [source,ruby] ---- json | yaml | toml do map 'key_value_model_attribute_name', to: :name_of_attribute, render_nil: true end ---- .Using the `render_nil` option to render empty attributes [example] ==== [source,ruby] ---- class Ceramic < Lutaml::Model::Serializable attribute :type, :string attribute :glaze, :string xml do map_element 'type', to: :type, render_nil: true map_element 'glaze', to: :glaze end json do map 'type', to: :type, render_nil: true map 'glaze', to: :glaze end end ---- [source,ruby] ---- > Ceramic.new.to_json > # { 'type': null } > Ceramic.new(type: "Porcelain", glaze: "Clear").to_json > # { 'type': 'Porcelain', 'glaze': 'Clear' } ---- [source,ruby] ---- > Ceramic.new.to_xml > # > Ceramic.new(type: "Porcelain", glaze: "Clear").to_xml > # PorcelainClear ---- ==== .Using the `render_nil` option to render empty attribute collections [example] ==== [source,ruby] ---- class Ceramic < Lutaml::Model::Serializable attribute :type, :string attribute :glazes, :string, collection: true xml do map_element 'type', to: :type, render_nil: true map_element 'glazes', to: :glazes, render_nil: true end json do map 'type', to: :type, render_nil: true map 'glazes', to: :glazes, render_nil: true end end ---- [source,ruby] ---- > Ceramic.new.to_json > # { 'type': null, 'glazes': [] } > Ceramic.new(type: "Porcelain", glazes: ["Clear"]).to_json > # { 'type': 'Porcelain', 'glazes': ['Clear'] } ---- [source,ruby] ---- > Ceramic.new.to_xml > # > Ceramic.new(type: "Porcelain", glazes: ["Clear"]).to_xml > # PorcelainClear ---- ==== === Advanced attribute mapping ==== Attribute mapping delegation Delegate attribute mappings to nested objects using the `delegate` option. Syntax: [source,ruby] ---- xml | json | yaml | toml do map 'key_value_model_attribute_name', to: :name_of_attribute, delegate: :model_to_delegate_to end ---- .Using the `delegate` option to map attributes to nested objects [example] ==== The following class will parse the JSON snippet below: [source,ruby] ---- class Glaze < Lutaml::Model::Serializable attribute :color, :string attribute :temperature, :integer json do map 'color', to: :color map 'temperature', to: :temperature end end class Ceramic < Lutaml::Model::Serializable attribute :type, :string attribute :glaze, Glaze json do map 'type', to: :type map 'color', to: :color, delegate: :glaze end end ---- [source,json] ---- { "type": "Porcelain", "color": "Clear" } ---- [source,ruby] ---- > Ceramic.from_json(json) > #> > Ceramic.new(type: "Porcelain", glaze: Glaze.new(color: "Clear")).to_json > #{"type"=>"Porcelain", "color"=>"Clear"} ---- ==== NOTE: The corresponding keyword used by Shale is `receiver:` instead of `delegate:`. ==== Attribute serialization with custom methods ===== General Define custom methods for specific attribute mappings using the `with:` key for each serialization mapping block for `from` and `to`. ===== XML serialization with custom methods Syntax: .XML serialization with custom methods [source,ruby] ---- xml do map_element 'element_name', to: :name_of_element, with: { to: :method_name_to_serialize, from: :method_name_to_deserialize } map_attribute 'attribute_name', to: :name_of_attribute, with: { to: :method_name_to_serialize, from: :method_name_to_deserialize } map_content, to: :name_of_content, with: { to: :method_name_to_serialize, from: :method_name_to_deserialize } end ---- .Using the `with:` key to define custom serialization methods for XML [example] ==== The following class will parse the XML snippet below: [source,ruby] ---- class CustomCeramic < Lutaml::Model::Serializable attribute :name, :string attribute :size, :integer attribute :description, :string xml do map_element "Name", to: :name, with: { to: :name_to_xml, from: :name_from_xml } map_attribute "Size", to: :size, with: { to: :size_to_xml, from: :size_from_xml } map_content with: { to: :description_to_xml, from: :description_from_xml } end def name_to_xml(model, parent, doc) el = doc.create_element("Name") doc.add_text(el, "XML Masterpiece: #{model.name}") doc.add_element(parent, el) end def name_from_xml(model, value) model.name = value.sub(/^XML Masterpiece: /, "") end def size_to_xml(model, parent, doc) doc.add_attribute(parent, "Size", model.size + 3) end def size_from_xml(model, value) model.size = value.to_i - 3 end def description_to_xml(model, parent, doc) doc.add_text(parent, "XML Description: #{model.description}") end def description_from_xml(model, value) model.description = value.join.strip.sub(/^XML Description: /, "") end end ---- [source,xml] ---- XML Masterpiece: Vase XML Description: A beautiful ceramic vase ---- [source,ruby] ---- > CustomCeramic.from_xml(xml) > # > puts CustomCeramic.new(name: "Vase", size: 12, description: "A beautiful vase").to_xml # # XML Masterpiece: Vase # XML Description: A beautiful vase # ---- ==== ===== Key-value data model serialization with custom methods .Key-value data model serialization with custom methods [source,ruby] ---- json | yaml | toml do map 'attribute_name', to: :name_of_attribute, with: { to: :method_name_to_serialize, from: :method_name_to_deserialize } end ---- .Using the `with:` key to define custom serialization methods [example] ==== The following class will parse the JSON snippet below: [source,ruby] ---- class CustomCeramic < Lutaml::Model::Serializable attribute :name, :string attribute :size, :integer json do map 'name', to: :name, with: { to: :name_to_json, from: :name_from_json } map 'size', to: :size end def name_to_json(model, doc) doc["name"] = "Masterpiece: #{model.name}" end def name_from_json(model, value) model.name = value.sub(/^Masterpiece: /, '') end end ---- [source,json] ---- { "name": "Masterpiece: Vase", "size": 12 } ---- [source,ruby] ---- > CustomCeramic.from_json(json) > # > CustomCeramic.new(name: "Vase", size: 12).to_json > #{"name"=>"Masterpiece: Vase", "size"=>12} ---- ==== [[attribute-extraction]] ==== Attribute extraction (for key-value data models only) NOTE: This feature is for key-value data model serialization only. The `child_mappings` option is used to extract results from a key-value data model (JSON, YAML, TOML) into a `Lutaml::Model` collection. The values are extracted from the key-value data model using the list of keys provided. Syntax: [source,ruby] ---- json | yaml | toml do map 'key_value_model_attribute_name', to: :name_of_attribute, child_mappings: { key_attribute_name_1: <1> {path_to_value_1}, <2> key_attribute_name_2: {path_to_value_2}, # ... } end ---- <1> The `key_attribute_name_1` is the attribute name in the model. The value of this attribute will be assigned the key of the hash in the key-value data model. <2> The `path_to_value_1` is an array of keys that represent the path to the value in the key-value data model. The keys are used to extract the value from the key-value data model and assign it to the attribute in the model. The `path_to_value` is in a nested array format with each value a symbol, where each symbol represents a key to traverse down. The last key in the path is the value to be extracted. .Determining the path to value in a key-value data model [example] ==== The following JSON contains 2 keys in schema named `engine` and `gearbox`. [source,json] ---- { "components": { "engine": { "manufacturer": "Ford", "model": "V8" }, "gearbox": { "manufacturer": "Toyota", "model": "4-speed" } } } ---- The path to value for the `engine` schema is `[:components, :engine]` and for the `gearbox` schema is `[:components, :gearbox]`. ==== In `path_to_value`, the `:key` and `:value` are reserved instructions used to assign the key or value of the serialization data respectively as the value to the attribute. [example] ==== In the following JSON content, the `path_to_value` for the object keys named `engine` and `gearbox` will utilize the `:key` keyword to assign the key of the object as the value of a designated attribute. [source,json] ---- { "components": { "engine": { /*...*/ }, "gearbox": { /*...*/ } } } ---- ==== If a specified value path is not found, the corresponding attribute in the model will be assigned a `nil` value. .Attribute values set to `nil` when the `path_to_value` is not found [example] ==== In the following JSON content, the `path_to_value` of `[:extras, :sunroof]` and `[:extras, :drinks_cooler]` at the object `"gearbox"` would be set to `nil`. [source,json] ---- { "components": { "engine": { "manufacturer": "Ford", "extras": { "sunroof": true, "drinks_cooler": true } }, "gearbox": { "manufacturer": "Toyota" } } } ---- ==== .Using the `child_mappings` option to extract values from a key-value data model [example] ==== The following JSON contains 2 keys in schema named `foo` and `bar`. [source,json] ---- { "schemas": { "foo": { <1> "path": { <2> "link": "link one", "name": "one" } }, "bar": { <1> "path": { <2> "link": "link two", "name": "two" } } } } ---- <1> The keys `foo` and `bar` are to be mapped to the `id` attribute. <2> The nested `path.link` and `path.name` keys are used as the `link` and `name` attributes, respectively. A model can be defined for this JSON as follows: [source,ruby] ---- class Schema < Lutaml::Model::Serializable attribute :id, :string attribute :link, :string attribute :name, :string end class ChildMappingClass < Lutaml::Model::Serializable attribute :schemas, Schema, collection: true json do map "schemas", to: :schemas, child_mappings: { id: :key, link: %i[path link], name: %i[path name], } end end ---- The output becomes: [source,ruby] ---- > ChildMappingClass.from_json(json) > #, #]> > ChildMappingClass.new(schemas: [Schema.new(id: "foo", link: "link one", name: "one"), Schema.new(id: "bar", link: "link two", name: "two")]).to_json > #{"schemas"=>{"foo"=>{"path"=>{"link"=>"link one", "name"=>"one"}}, {"bar"=>{"path"=>{"link"=>"link two", "name"=>"two"}}}} ---- In this example: * The `key` of each schema (`foo` and `bar`) is mapped to the `id` attribute. * The nested `path.link` and `path.name` keys are mapped to the `link` and `name` attributes, respectively. ==== == Validation === General Lutaml::Model provides a way to validate data models using the `validate` and `validate!` methods. * The `validate` method sets an `errors` array in the model instance that contains all the validation errors. This method is used for checking the validity of the model silently. * The `validate!` method raises a `Lutaml::Model::ValidationError` that contains all the validation errors. This method is used for forceful validation of the model through raising an error. Lutaml::Model supports the following validation methods: * `collection`:: Validates collection size range. * `values`:: Validates the value of an attribute from a set of fixed values. [example] ==== The following class will validate the `degree_settings` attribute to ensure that it has at least one element and that the `description` attribute is one of the values in the set `[one, two, three]`. [source,ruby] ---- class Klin < Lutaml::Model::Serializable attribute :name, :string attribute :degree_settings, :integer, collection: (1..) attribute :description, :string, values: %w[one two three] xml do map_element 'name', to: :name map_attribute 'degree_settings', to: :degree_settings end end klin = Klin.new(name: "Klin", degree_settings: [100, 200, 300], description: "one") klin.validate # => [] klin = Klin.new(name: "Klin", degree_settings: [], description: "four") klin.validate # => [ # #, # # # ] e = klin.validate! # => Lutaml::Model::ValidationError: [ # degree_settings must have at least 1 element, # description must be one of [one, two, three] # ] e.errors # => [ # #, # # # ] ---- ==== === Custom validation To add custom validation, override the `validate` method in the model class. Additional errors should be added to the `errors` array. [example] ==== The following class validates the `degree_settings` attribute when the `type` is `glass` to ensure that the value is less than 1300. [source,ruby] ---- class Klin < Lutaml::Model::Serializable attribute :name, :string attribute :type, :string, values: %w[glass ceramic] attribute :degree_settings, :integer, collection: (1..) def validate errors = super if type == "glass" && degree_settings.any? { |d| d > 1300 } errors << Lutaml::Model::Error.new("Degree settings for glass must be less than 1300") end end end klin = Klin.new(name: "Klin", type: "glass", degree_settings: [100, 200, 1400]) klin.validate # => [#] ---- ==== == Adapters === General Lutaml::Model uses an adapter pattern to support multiple libraries for each serialization format. You will need to specify the configuration for the adapter you want to use. The easiest way is to copy and paste the following configuration into your code. The configuration is as follows: [source,ruby] ---- require 'lutaml/model' require 'lutaml/model/xml_adapter/nokogiri_adapter' require 'lutaml/model/json_adapter/standard_json_adapter' require 'lutaml/model/toml_adapter/toml_rb_adapter' require 'lutaml/model/yaml_adapter/standard_yaml_adapter' Lutaml::Model::Config.configure do |config| config.xml_adapter = Lutaml::Model::XmlAdapter::NokogiriAdapter config.yaml_adapter = Lutaml::Model::YamlAdapter::StandardYamlAdapter config.json_adapter = Lutaml::Model::JsonAdapter::StandardJsonAdapter config.toml_adapter = Lutaml::Model::TomlAdapter::TomlRbAdapter end ---- You can also provide the adapter type by using symbols like [source,ruby] ---- require 'lutaml/model' Lutaml::Model::Config.configure do |config| config.xml_adapter_type = :nokogiri # can be one of [:nokogiri, :ox, :oga] config.yaml_adapter_type = :standard_yaml config.json_adapter_type = :standard_json # can be one of [:standard_json, :multi_json] config.toml_adapter_type = :toml_rb # can be one of [:toml_rb, :tomlib] end ---- NOTE: By default `yaml_adapter_type` and `json_adapter_type` are set to `:standard_yaml` and `:standard_json` respectively. === XML Lutaml::Model supports the following XML adapters: * Nokogiri (default) * Oga (optional, plain Ruby suitable for Opal/JS) * Ox (optional) .Using the Nokogiri XML adapter [source,ruby] ---- require 'lutaml/model' Lutaml::Model::Config.configure do |config| require 'lutaml/model/xml_adapter/nokogiri_adapter' config.xml_adapter = Lutaml::Model::XmlAdapter::NokogiriAdapter end ---- .Using the Oga XML adapter [source,ruby] ---- require 'lutaml/model' Lutaml::Model::Config.configure do |config| require 'lutaml/model/xml_adapter/oga_adapter' config.xml_adapter = Lutaml::Model::XmlAdapter::OgaAdapter end ---- .Using the Ox XML adapter [source,ruby] ---- require 'lutaml/model' Lutaml::Model::Config.configure do |config| require 'lutaml/model/xml_adapter/ox_adapter' config.xml_adapter = Lutaml::Model::XmlAdapter::OxAdapter end ---- === YAML Lutaml::Model supports only one YAML adapter. * YAML (default) .Using the YAML adapter [source,ruby] ---- require 'lutaml/model' Lutaml::Model::Config.configure do |config| require 'lutaml/model/yaml_adapter/standard_yaml_adapter' config.yaml_adapter = Lutaml::Model::YamlAdapter::StandardYamlAdapter end ---- === JSON Lutaml::Model supports the following JSON adapters: * JSON (default) * MultiJson (optional) .Using the JSON adapter [source,ruby] ---- require 'lutaml/model' Lutaml::Model::Config.configure do |config| require 'lutaml/model/json_adapter/standard_json_adapter' config.json_adapter = Lutaml::Model::JsonAdapter::StandardJsonAdapter end ---- .Using the MultiJson adapter [source,ruby] ---- require 'lutaml/model' Lutaml::Model::Config.configure do |config| require 'lutaml/model/json_adapter/multi_json_adapter' config.json_adapter = Lutaml::Model::JsonAdapter::MultiJsonAdapter end ---- === TOML Lutaml::Model supports the following TOML adapters: * Toml-rb (default) * Tomlib (optional) .Using the Toml-rb adapter [source,ruby] ---- require 'lutaml/model' Lutaml::Model::Config.configure do |config| require 'lutaml/model/toml_adapter/toml_rb_adapter' config.toml_adapter = Lutaml::Model::TomlAdapter::TomlRbAdapter end ---- .Using the Tomlib adapter [source,ruby] ---- require 'lutaml/model' Lutaml::Model::Config.configure do |config| config.toml_adapter = Lutaml::Model::TomlAdapter::TomlibAdapter require 'lutaml/model/toml_adapter/tomlib_adapter' end ---- == Comparison with Shale Lutaml::Model is a serialization library that is similar to Shale, but with some differences in implementation. [cols="a,a,a,a",options="header"] |=== | Feature | Lutaml::Model | Shale | Notes | Data model definition | 3 types: * <> * <> * <> | 2 types: * Inherit from `Shale::Mapper` * Custom model class | | Value types | `Lutaml::Model::Type` includes: `Integer`, `String`, `Float`, `Boolean`, `Date`, `DateTime`, `Time`, `Decimal`, `Hash`. | `Shale::Type` includes: `Integer`, `String`, `Float`, `Boolean`, `Date`, `Time`. | Lutaml::Model supports additional value types `Decimal`, `DateTime` and `Hash`. | Configuration | `Lutaml::Model::Config` | `Shale.{type}_adapter` | Lutaml::Model uses a configuration block to set the serialization adapters. | Custom serialization methods | `:with`, on individual attributes | `:using`, on entire object/document | Lutaml::Model uses the `:with` keyword for custom serialization methods. | Serialization formats | XML, YAML, JSON, TOML | XML, YAML, JSON, TOML, CSV | Lutaml::Model does not support CSV. | Validation | Supports collection range, fixed values, and custom validation | Requires implementation | | Adapter support | XML (Nokogiri, Ox, Oga), YAML, JSON (JSON, MultiJson), TOML (Toml-rb, Tomlib) | XML (Nokogiri, Ox), YAML, JSON (JSON, MultiJson), TOML (Toml-rb, Tomlib), CSV | Lutaml::Model does not support CSV. 4+h| XML features | <> | Yes. Supports `` through the `namespace` option without prefix. | No. Only supports ``. | | XML mixed content support | Yes. Supports the following kind of XML through <> support. [source,xml] ---- My name is John Doe, and I'm 28 years old ---- | No. Shale's `map_content` only supports the first text node. | | XML namespace inheritance | Yes. Supports the <> option to inherit the namespace from the root element. | No. | | Support for `xsi:schemaLocation` | Yes. Automatically supports the <> attribute for every element. | Requires manual specification on every XML element that uses it. | 4+h| Attribute features | Attribute delegation | `:delegate` option to delegate attribute mappings to a model. | `:receiver` option to delegate attribute mappings to a model. | | Enumerations | Yes. Supports enumerations as value types through the <>. | No. | Lutaml::Model supports enumerations as value types. | Attribute extraction | Yes. Supports <> from key-value data models. | No. | Lutaml::Model supports attribute extraction from key-value data models. |=== [[migrate-from-shale]] == Migration steps from Shale The following sections provide a guide for migrating from Shale to Lutaml::Model. === Step 1: Replace inheritance class `Lutaml::Model` uses `Lutaml::Model::Serializable` as the base inheritance class. [source,ruby] ---- class Example < Lutaml::Model::Serializable # ... end ---- [NOTE] ==== `Lutaml::Model` also supports an inclusion method as in the following example, which is not supported by Shale. This is useful for cases where you want to include the serialization methods in a class that already inherits from another class. [source,ruby] ---- class Example include Lutaml::Model::Serialize # ... end ---- ==== Shale uses `Shale::Mapper` as the base inheritance class. [source,ruby] ---- class Example < Shale::Mapper # ... end ---- Actions: * Replace mentions of `Shale::Mapper` with `Lutaml::Model::Serializable`. * Potentially replace inheritance with inclusion for suitable cases. === Step 2: Replace value type definitions Value types in `Lutaml::Model` are under the `Lutaml::Model::Type` module, or use the LutaML type symbols. [source,ruby] ---- class Example < Lutaml::Model::Serializable attribute :length, :integer attribute :description, :string end ---- [NOTE] ==== `Lutaml::Model` supports specifying predefined value types as strings or symbols, which is not supported by Shale. [source,ruby] ---- class Example < Lutaml::Model::Serializable attribute :length, Lutaml::Model::Type::Integer attribute :description, "String" end ---- ==== Value types in Shale are under the `Shale::Type` module. [source,ruby] ---- class Example < Shale::Mapper attribute :length, Shale::Type::Integer attribute :description, Shale::Type::String end ---- Action: * Replace mentions of `Shale::Type` with `Lutaml::Model::Type`. * Potentially replace value type definitions with strings or symbols. === Step 3: Configure serialization adapters `Lutaml::Model` uses a configuration block to set the serialization adapters. [source,ruby] ---- require 'lutaml/model/xml_adapter/nokogiri_adapter' Lutaml::Model::Config.configure do |config| config.xml_adapter = Lutaml::Model::XmlAdapter::NokogiriAdapter end ---- The equivalent for Shale is this: [source,ruby] ---- require 'shale/adapter/nokogiri' Shale.xml_adapter = Shale::Adapter::Nokogiri ---- Here are places that this code may reside at: * If your code is a standalone Ruby script, this code will be present in your code. * If your code is organized in a Ruby gem, this code will be specified somewhere referenced by `lib/your_gem_name.rb`. * If your code contains tests or specs, they will be in the test setup file, e.g. RSpec `spec/spec_helper.rb`. Actions: * Replace the Shale configuration block with the `Lutaml::Model::Config` configuration block. * Replace the Shale adapter with the `Lutaml::Model` adapter. === Step 4: Rewrite custom serialization methods There is an implementation difference between Lutaml::Model and Shale for custom serialization methods. Custom serialization methods in `Lutaml::Model` map to individual attributes. For custom serialization methods, Lutaml::Model uses the `:with` keyword instead of the `:using` keyword used by Shale. [source,ruby] ---- class Example < Lutaml::Model::Serializable attribute :name, :string attribute :size, :integer attribute :color, :string attribute :description, :string json do map "name", to: :name, with: { to: :name_to_json, from: :name_from_json } map "size", to: :size map "color", to: :color, with: { to: :color_to_json, from: :color_from_json } map "description", to: :description, with: { to: :description_to_json, from: :description_from_json } end xml do root "CustomSerialization" map_element "Name", to: :name, with: { to: :name_to_xml, from: :name_from_xml } map_attribute "Size", to: :size map_element "Color", to: :color, with: { to: :color_to_xml, from: :color_from_xml } map_content to: :description, with: { to: :description_to_xml, from: :description_from_xml } end def name_to_json(model, doc) doc["name"] = "JSON Masterpiece: #{model.name}" end def name_from_json(model, value) model.name = value.sub(/^JSON Masterpiece: /, "") end def color_to_json(model, doc) doc["color"] = model.color.upcase end def color_from_json(model, value) model.color = value.downcase end def description_to_json(model, doc) doc["description"] = "JSON Description: #{model.description}" end def description_from_json(model, value) model.description = value.sub(/^JSON Description: /, "") end def name_to_xml(model, parent, doc) el = doc.create_element("Name") doc.add_text(el, "XML Masterpiece: #{model.name}") doc.add_element(parent, el) end def name_from_xml(model, value) model.name = value.sub(/^XML Masterpiece: /, "") end def color_to_xml(model, parent, doc) color_element = doc.create_element("Color") doc.add_text(color_element, model.color.upcase) doc.add_element(parent, color_element) end def color_from_xml(model, value) model.color = value.downcase end def description_to_xml(model, parent, doc) doc.add_text(parent, "XML Description: #{model.description}") end def description_from_xml(model, value) model.description = value.join.strip.sub(/^XML Description: /, "") end end ---- Custom serialization methods in Shale do not map to specific attributes, but allow the user to specify where the data goes. [source,ruby] ---- class Example < Shale::Mapper attribute :name, Shale::Type::String attribute :size, Shale::Type::Integer attribute :color, Shale::Type::String attribute :description, Shale::Type::String json do map "name", using: { from: :name_from_json, to: :name_to_json } map "size", to: :size map "color", using: { from: :color_from_json, to: :color_to_json } map "description", to: :description, using: { from: :description_from_json, to: :description_to_json } end xml do root "CustomSerialization" map_element "Name", using: { from: :name_from_xml, to: :name_to_xml } map_attribute "Size", to: :size map_element "Color", using: { from: :color_from_xml, to: :color_to_xml } map_content to: :description, using: { from: :description_from_xml, to: :description_to_xml } end def name_to_json(model, doc) doc['name'] = "JSON Masterpiece: #{model.name}" end def name_from_json(model, value) model.name = value.sub(/^JSON Masterpiece: /, "") end def color_to_json(model, doc) doc['color'] = model.color.upcase end def color_from_json(model, doc) model.color = doc['color'].downcase end def description_to_json(model, doc) doc['description'] = "JSON Description: #{model.description}" end def description_from_json(model, doc) model.description = doc['description'].sub(/^JSON Description: /, "") end def name_from_xml(model, node) model.name = node.text.sub(/^XML Masterpiece: /, "") end def name_to_xml(model, parent, doc) name_element = doc.create_element('Name') doc.add_text(name_element, model.street.to_s) doc.add_element(parent, name_element) end end ---- NOTE: There are cases where the Shale implementation of custom methods work differently from the Lutaml::Model implementation. In these cases, you will need to adjust the custom methods accordingly. Actions: * Replace the `using` keyword with the `with` keyword. * Adjust the custom methods. == About LutaML The name "LutaML" is pronounced as "Looh-tah-mel". The name "LutaML" comes from the Latin word for clay, "Lutum", and "ML" for "Markup Language". Just as clay can be molded and modeled into beautiful and practical end products, the Lutaml::Model gem is used for data modeling, allowing you to shape and structure your data into useful forms. == License and Copyright This project is licensed under the BSD 2-clause License. See the link:LICENSE.md[] file for details. Copyright Ribose.