github.com/Crystal-Matter/tlv
2.0.0 / published Sep 12, 2026 / repository
Matter TLV encoder/decoder
TLV
Matter TLV encoder/decoder
Installation
-
Add the dependency to your
shard.yml:dependencies: tlv: github: spider-gazelle/tlv -
Run
shards install
Usage
require "tlv"
class User
include TLV::Serializable
@[TLV::Field(tag: 1)]
property first_name : String
@[TLV::Field(tag: 2)]
property last_name : String
end
class Packet
include TLV::Serializable
@[TLV::Field(tag: 1)]
property id : UInt8
@[TLV::Field(tag: 2)]
property port : UInt16
@[TLV::Field(tag: 3)]
property? duplex : Bool
@[TLV::Field(tag: 4)]
property message : String
@[TLV::Field(tag: 5)]
property encoded_message : Bytes
# arrays
@[TLV::Field(tag: 6)]
property array : Array(TLV::Value)
# lists
@[TLV::Field(tag: 1)]
property list : Tuple(UInt8, String, UInt16)
# Tuple serialized as TLV Array (homogeneous format)
@[TLV::Field(tag: 1, container: :array)]
property items : Tuple(UInt8, UInt8, UInt8)
# Array serialized as TLV List (heterogeneous format)
@[TLV::Field(tag: 1, container: :list)]
property items : Array(UInt8)
# Nested structures
@[TLV::Field(tag: 7)]
property user : User
@[TLV::Field(tag: 8)]
property optional_field : String?
# nilable required field
@[TLV::Field(tag: 9, optional: false)]
property not_optional_field : String?
# Common Profile Tag
@[TLV::Field(tag: {0x235A, 42})]
property common : UInt32
# Vendor Profile Tag
@[TLV::Field(tag: {0xFFFF, 0x235A, 42})]
property vendor : UInt32
end
io = IO::Memory.new # bytes from network etc
packet = io.read_bytes(Packet)
packet.to_slice
There is also a TLV::Any type which should really only be used externally for payloads with an anonymous type. (i.e. no wrapping structure)
require "tlv"
io = IO::Memory.new(Bytes[0x05, 0xF1, 0xFF]) # Anonymous UInt16 value 65521
any = io.read_bytes(TLV::Any)
any.header.element_type # => TLV::ElementType::UnsignedInt16
any.as_u16 # => 65521_u16
Custom field types
A type that includes TLV::Serializable encodes as a structure. For a value that
is a bare scalar on the wire, such as a wrapper around an identifier, register a
pair of overloads instead and the type can then be used as a field, including as
a nilable field or a member of a union.
struct NodeId
getter id : UInt64
def initialize(@id : UInt64)
end
def to_tlv(outer_tag : TLV::TagId? = nil) : TLV::Any
TLV::Any.new(@id, outer_tag)
end
def self.from_tlv(any : TLV::Any) : NodeId
new(TLV::Serializable.deserialize_value(any, UInt64))
end
end
module TLV::Serializable
def self.serialize_value(value : NodeId, tag, fixed_size : Bool = false) : TLV::Any
value.to_tlv(tag)
end
def self.deserialize_value(any : TLV::Any, type : NodeId.class) : NodeId
type.from_tlv(any)
end
end
Errors
Malformed input raises TLV::DeserializationError, carrying the structure and
field it failed on. A value that cannot be written raises
TLV::SerializationError, which is what an unregistered field type produces.
Contributing
- Fork it (https://github.com/spider-gazelle/tlv/fork)
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
Contributors
- Stephen von Takach - creator and maintainer
API
- Array(T)
An
Arrayis an ordered, integer-indexed collection of objects of type T. - Bool
Bool has only two possible values:
trueandfalse. - Float32
- Float64
- Int16
- Int32
- Int64
- Int8
- Nil
The
Niltype has only one possible value:nil. - Slice(T)
A
Sliceis aPointerwith an associated size. - String
A
Stringrepresents an immutable sequence of UTF-8 characters. - TLV
- Tuple(*T)
A tuple is a fixed-size, immutable, stack-allocated sequence of values of possibly different types.
- UInt16
- UInt32
- UInt64
- UInt8