
Type-safe, serializable heterogeneous map with typed keys, mutable/immutable variants, automatic JSON serialization, schema-driven validation, eager/lazy deserialization, and class-keyed polymorphic maps.
Type-safe, serializable, heterogeneous map.
DynoKey<T>).Json format of kotlinx.serialization.// Define typed keys
object Person {
val name by dynoKey<String>()
val age by dynoKey<Int>()
val emails by dynoKey<List<String>?>()
}
val person = dynamicObjectOf(
Person.name with "Alex",
Person.age with 42,
Person.emails with listOf("alex@example.com")
)
// Type-safe accessors
val name = person[Person.name] // String
val age = person[Person.age] // Int
val emails = person[Person.emails] // List<String>?
// Serialization support
val json = Json.encodeToString(person)
val restored = Json.decodeFromString<DynamicObject>(json)Serialized form:
{
"name": "Alex",
"age": 31,
"emails": ["alex@example.com"]
}implementation("io.github.adokky:dyno-core:0.12.0")A typed key used to access values in DynamicObject and MutableDynamicObject.
Can be instantiated directly or by using a delegate dynoKey.
object Person {
val id = DynoKey<Int>("id")
val name by dynoKey<String>()
val age by dynoKey<Int?>()
}The main class representing an immutable, type-safe, and serializable heterogeneous map. It allows storing and retrieving values of different types using typed keys (DynoKey).
Example:
val obj = dynamicObjectOf(
Person.id with 42,
Person.name with "Alex",
Person.age with 30
)
val name: Int? = obj[Person.age]If the type parameter T in DynoKey<T> is not nullable, then the return type is also non-nullable.
NoSuchDynoKeyException is thrown if the key does not exist:
val id: Int = obj[Person.id]A mutable variant of DynamicObject that allows adding, updating, and removing key-value pairs. Shares the same type-safety and serialization features.
val obj = mutableDynamicObjectOf(Person.name with "Bob")
obj[Person.age] = 25
obj -= Person.nameimplementation("io.github.adokky:dyno-classmap:0.12.0")A specialized map implementation that associates T / KClass<T> keys with values of serializable type T.
val map: ClassMap = buildClassMap {
put("foo")
put(42)
}
map.get<String>() // returns "foo"
map.get<Int>() // returns 42In JSON, serial name of T used as a key:
@Serializable
@SerialName("user")
data class User(val name: String, val age: Int)
@Serializable
@SerialName("account")
data class Account(val id: Int, val active: Boolean)
val map: ClassMap = buildClassMap {
put(User("Alex", 30))
put(Account(123, true))
}
Json.encodeToString(ClassMapSerializer, map){
"user": {
"name": "Alex",
"age": 30
},
"account": {
"id": 123,
"active": true
}
}For working with any DynoMap (including DynamicObject), functions like getInstance/setInstance are available, for example:
val obj = mutableDynamicObjectOf()
obj.setInstance(User("Bob", 25))
val user: User? = obj.getInstance()These functions allow interacting with class instances without explicitly specifying keys. However:
ClassMap only accept classes/types as the keys, while DynoMap is only restricted by its type argument.ClassMap provides more ergonomics by leveraging standard method names and built-in operatorsA variant of ClassMap that restricts keys to only subclasses of a specified base class T.
abstract class Animal
@Serializable data class Dog(val name: String) : Animal()
@Serializable data class Cat(val name: String) : Animal()
val map: TypedClassMap<Animal> = buildTypedClassMap {
put(Dog("Buddy"))
put(Cat("Whiskers"))
}
val dog: Dog? = map.get<Dog>()
val cat: Cat? = map.get<Cat>()
// compilation error: String is not a subtype of Animal
map.put("string")The dyno-schema module provides a powerful way to define and work with structured, validated dynamic objects using schemas. It brings compile-time safety and structural validation to DynamicObject.
object Person : SimpleDynoSchema("person") {
val name by dynoKey<String>()
val age by dynoKey<Int>()
}
// Create structurally validated entity.
// All required fields must be present.
val person = Person.new {
name set "Alex"
age set 30
}
// Throws MissingFieldsException
Json.decodeFromString(Person, """{"age":30}""")
person[Person.name] // OK
person[OtherSchema.name] // compilation errorFor full documentation and usage examples, see schema/README.md.
Dyno allows both lazy and eager deserialization modes, allowing you to choose the approach that best fits your performance and usability requirements.
The following classes are automatically serializable without any extra steps:
DynamicObjectMutableDynamicObjectClassMapMutableClassMapThe following classes have type arguments, so you may need to specify an explicit serializer if type argument is not serializable:
DynoMapMutableDynoMapTypedClassMapMutableTypedClassMapNote: Annotations like
@Serializable(with=Serializer::class)or@Contextualdo not work inside function type arguments. For example, this will not work:Json.decodeFromString<TypedClassMap<@Contextual Any>>("{}")
To serialize these classes using Json.encodeToString or Json.decodeFromString, you must specify an explicit serializer. For example, MutableClassMap should be serialized like this:
Json.encodeToString(MutableTypedClassMapSerializer, mutableTypedClassMap)| Class | Serializer |
|---|---|
| TypedClassMap | TypedClassMapSerializer |
| MutableTypedClassMap | MutableTypedClassMapSerializer |
| DynoMap | DynoMapMapSerializer |
| MutableDynoMap | MutableDynoMapSerializer |
Achieved with AbstractEagerDynoSerializer — a powerful base class for implementing eager deserialization strategies.
JsonElement representations, eager serialization decodes values directly into their final types, offering better memory efficiency.JsonElement) deserialization strategies within the same object by returning ResolveResult.Keep.ResolveResult.Delay, enabling polymorphic or conditional deserialization.Usage example.
The onAssign and onDecode processors can be assigned to add validation logic.
All processors are chained in the order of assignment.
onAssign is called when a value is manually assigned to the key (e.g., obj[key] = value or dynamicObjectOf(key with value)).onDecode is called when a value is deserialized.
Useful when validation is only needed for deserialized objects received from network.validate assigns the same validation logic to both onAssign and onDecode.object Person {
val age by dynoKey<Int>().onDecode {
require(it > 0) { "'age' must be positive, but was: $it" }
}
val name by dynoKey<String>().validate {
require(it.isNotBlank()) { "'name' must not be empty" }
}
}Decoding from JSON - both onDecode and validate processors are called:
val obj = Json.decodeFromString<DynamicObject>("""{"name": "", "age": -1}""")
decoded[Person.age] // throws IllegalArgumentException
decoded[Person.name] // throws IllegalArgumentExceptionManual assignment - only validate processor is called:
val obj = mutableDynamicObjectOf(Person.age with -1)
obj[Person.age] // returns -1
obj[Person.name] = "" // throws IllegalArgumentException
// throws IllegalArgumentException
mutableDynamicObjectOf(Person.name with "") The validation functions are easily composable and can be used to build your own validation DSL:
fun <R: DynoKeySpec<String>> R.notBlank() = validate {
require(it.isNotBlank()) { "property '$name' must not be empty" }
}
fun <R: DynoKeySpec<String>> R.maxLength(max: Int) = validate {
require(it.length <= max) { "property '$name' length must be <= $max, but was: ${it.length}" }
}Multiple validators are chained together:
object User {
val name = DynoKey<String>("username")
.notBlank()
.maxLength(100)
val email by dynoKey<String>()
.validate { require("@" in it) { "property '$name' must be valid email" } }
.maxLength(255)
}When a value is assigned or decoded, all validators in the chain are executed in order.
Type-safe, serializable, heterogeneous map.
DynoKey<T>).Json format of kotlinx.serialization.// Define typed keys
object Person {
val name by dynoKey<String>()
val age by dynoKey<Int>()
val emails by dynoKey<List<String>?>()
}
val person = dynamicObjectOf(
Person.name with "Alex",
Person.age with 42,
Person.emails with listOf("alex@example.com")
)
// Type-safe accessors
val name = person[Person.name] // String
val age = person[Person.age] // Int
val emails = person[Person.emails] // List<String>?
// Serialization support
val json = Json.encodeToString(person)
val restored = Json.decodeFromString<DynamicObject>(json)Serialized form:
{
"name": "Alex",
"age": 31,
"emails": ["alex@example.com"]
}implementation("io.github.adokky:dyno-core:0.12.0")A typed key used to access values in DynamicObject and MutableDynamicObject.
Can be instantiated directly or by using a delegate dynoKey.
object Person {
val id = DynoKey<Int>("id")
val name by dynoKey<String>()
val age by dynoKey<Int?>()
}The main class representing an immutable, type-safe, and serializable heterogeneous map. It allows storing and retrieving values of different types using typed keys (DynoKey).
Example:
val obj = dynamicObjectOf(
Person.id with 42,
Person.name with "Alex",
Person.age with 30
)
val name: Int? = obj[Person.age]If the type parameter T in DynoKey<T> is not nullable, then the return type is also non-nullable.
NoSuchDynoKeyException is thrown if the key does not exist:
val id: Int = obj[Person.id]A mutable variant of DynamicObject that allows adding, updating, and removing key-value pairs. Shares the same type-safety and serialization features.
val obj = mutableDynamicObjectOf(Person.name with "Bob")
obj[Person.age] = 25
obj -= Person.nameimplementation("io.github.adokky:dyno-classmap:0.12.0")A specialized map implementation that associates T / KClass<T> keys with values of serializable type T.
val map: ClassMap = buildClassMap {
put("foo")
put(42)
}
map.get<String>() // returns "foo"
map.get<Int>() // returns 42In JSON, serial name of T used as a key:
@Serializable
@SerialName("user")
data class User(val name: String, val age: Int)
@Serializable
@SerialName("account")
data class Account(val id: Int, val active: Boolean)
val map: ClassMap = buildClassMap {
put(User("Alex", 30))
put(Account(123, true))
}
Json.encodeToString(ClassMapSerializer, map){
"user": {
"name": "Alex",
"age": 30
},
"account": {
"id": 123,
"active": true
}
}For working with any DynoMap (including DynamicObject), functions like getInstance/setInstance are available, for example:
val obj = mutableDynamicObjectOf()
obj.setInstance(User("Bob", 25))
val user: User? = obj.getInstance()These functions allow interacting with class instances without explicitly specifying keys. However:
ClassMap only accept classes/types as the keys, while DynoMap is only restricted by its type argument.ClassMap provides more ergonomics by leveraging standard method names and built-in operatorsA variant of ClassMap that restricts keys to only subclasses of a specified base class T.
abstract class Animal
@Serializable data class Dog(val name: String) : Animal()
@Serializable data class Cat(val name: String) : Animal()
val map: TypedClassMap<Animal> = buildTypedClassMap {
put(Dog("Buddy"))
put(Cat("Whiskers"))
}
val dog: Dog? = map.get<Dog>()
val cat: Cat? = map.get<Cat>()
// compilation error: String is not a subtype of Animal
map.put("string")The dyno-schema module provides a powerful way to define and work with structured, validated dynamic objects using schemas. It brings compile-time safety and structural validation to DynamicObject.
object Person : SimpleDynoSchema("person") {
val name by dynoKey<String>()
val age by dynoKey<Int>()
}
// Create structurally validated entity.
// All required fields must be present.
val person = Person.new {
name set "Alex"
age set 30
}
// Throws MissingFieldsException
Json.decodeFromString(Person, """{"age":30}""")
person[Person.name] // OK
person[OtherSchema.name] // compilation errorFor full documentation and usage examples, see schema/README.md.
Dyno allows both lazy and eager deserialization modes, allowing you to choose the approach that best fits your performance and usability requirements.
The following classes are automatically serializable without any extra steps:
DynamicObjectMutableDynamicObjectClassMapMutableClassMapThe following classes have type arguments, so you may need to specify an explicit serializer if type argument is not serializable:
DynoMapMutableDynoMapTypedClassMapMutableTypedClassMapNote: Annotations like
@Serializable(with=Serializer::class)or@Contextualdo not work inside function type arguments. For example, this will not work:Json.decodeFromString<TypedClassMap<@Contextual Any>>("{}")
To serialize these classes using Json.encodeToString or Json.decodeFromString, you must specify an explicit serializer. For example, MutableClassMap should be serialized like this:
Json.encodeToString(MutableTypedClassMapSerializer, mutableTypedClassMap)| Class | Serializer |
|---|---|
| TypedClassMap | TypedClassMapSerializer |
| MutableTypedClassMap | MutableTypedClassMapSerializer |
| DynoMap | DynoMapMapSerializer |
| MutableDynoMap | MutableDynoMapSerializer |
Achieved with AbstractEagerDynoSerializer — a powerful base class for implementing eager deserialization strategies.
JsonElement representations, eager serialization decodes values directly into their final types, offering better memory efficiency.JsonElement) deserialization strategies within the same object by returning ResolveResult.Keep.ResolveResult.Delay, enabling polymorphic or conditional deserialization.Usage example.
The onAssign and onDecode processors can be assigned to add validation logic.
All processors are chained in the order of assignment.
onAssign is called when a value is manually assigned to the key (e.g., obj[key] = value or dynamicObjectOf(key with value)).onDecode is called when a value is deserialized.
Useful when validation is only needed for deserialized objects received from network.validate assigns the same validation logic to both onAssign and onDecode.object Person {
val age by dynoKey<Int>().onDecode {
require(it > 0) { "'age' must be positive, but was: $it" }
}
val name by dynoKey<String>().validate {
require(it.isNotBlank()) { "'name' must not be empty" }
}
}Decoding from JSON - both onDecode and validate processors are called:
val obj = Json.decodeFromString<DynamicObject>("""{"name": "", "age": -1}""")
decoded[Person.age] // throws IllegalArgumentException
decoded[Person.name] // throws IllegalArgumentExceptionManual assignment - only validate processor is called:
val obj = mutableDynamicObjectOf(Person.age with -1)
obj[Person.age] // returns -1
obj[Person.name] = "" // throws IllegalArgumentException
// throws IllegalArgumentException
mutableDynamicObjectOf(Person.name with "") The validation functions are easily composable and can be used to build your own validation DSL:
fun <R: DynoKeySpec<String>> R.notBlank() = validate {
require(it.isNotBlank()) { "property '$name' must not be empty" }
}
fun <R: DynoKeySpec<String>> R.maxLength(max: Int) = validate {
require(it.length <= max) { "property '$name' length must be <= $max, but was: ${it.length}" }
}Multiple validators are chained together:
object User {
val name = DynoKey<String>("username")
.notBlank()
.maxLength(100)
val email by dynoKey<String>()
.validate { require("@" in it) { "property '$name' must be valid email" } }
.maxLength(255)
}When a value is assigned or decoded, all validators in the chain are executed in order.