Schemas

Alongside the codec (which round-trips bytes), each collection records a schema describing the shape of what it stores. Schemas are what let tools — the catalog, the SQL DESCRIBE, and drift detection — understand a collection without reading values.

Schema support is new in 3.x. It is opt-in for value types and automatic for keys, so existing code keeps compiling; deriving LMDBSchema simply makes a collection self-describing.

Table of contents

  1. LMDBSchema[T]
  2. Keys are always described
  3. Values: derive for a rich schema
  4. SchemaShape
  5. SchemaArtifact
  6. Custom schema for a custom codec
  7. How the SQL layer uses it

LMDBSchema[T]

LMDBSchema[T] is a small typeclass that yields a persisted SchemaArtifact for a type. It is kept separate from LMDBCodec[T] on purpose: the codec cares about bytes, the schema cares about describing structure. A custom codec can therefore declare a matching schema without changing the codec API.

trait LMDBSchema[T]:
  def artifact: SchemaArtifact

collectionCreate[K, T] requires both an LMDBSchema[K] and an LMDBSchema[T]. Both are resolved implicitly, so in practice you never pass them by hand:

def collectionCreate[K, T](name: CollectionName, failIfExists: Boolean = true)(using
  KeyCodec[K], LMDBCodec[T], LMDBSchema[K], LMDBSchema[T]
): IO[CreateErrors, LMDBCollection[K, T]]

Keys are always described

A key always has a KeyCodec[K], and every codec carries a stable keyId (e.g. lmdb:int64, lmdb-geo:location/v1). So a key’s schema is automatically a KeySchema(keyId) — no derivation needed. This is what lets the catalog distinguish byte-compatible but semantically different keys: Int (lmdb:int32) vs Long (lmdb:int64), UUID vs ULID vs UUIDv7, and so on.


Values: derive for a rich schema

A value type opts in with derives LMDBSchema. The derivation produces a JsonSchema whose payload is the structural shape of the type:

import zio.lmdb.json.*
import zio.lmdb.schema.*

case class User(name: String, age: Int, nickname: Option[String])
  derives LMDBCodecJson, LMDBSchema

Without derives LMDBSchema, a value type falls back to a low-priority opaque schema (OpaqueSchema). It still works, but downstream layers surface it as a “shape unknown” warning rather than a precise description, and DESCRIBE cannot list its columns.


SchemaShape

SchemaShape[T] is the compile-time, Mirror-based derivation behind derives LMDBSchema. It walks the model — no runtime reflection, no extra dependency — and emits a lightweight JSON-Schema-like JValue tree:

Scala Shape
String / Boolean {"type":"string"} / {"type":"boolean"}
Int / Long / BigInt {"type":"integer"}
Float / Double / BigDecimal {"type":"number"}
Instant / OffsetDateTime {"type":"string","format":"date-time"}
UUID {"type":"string","format":"uuid"}
case class {"type":"object","properties":{…},"required":[…]}
sealed trait / enum {"oneOf":[…]}
Option[T] the inner shape, field dropped from required
Seq/List/Vector/Set/Array[T] {"type":"array","items":…}
Map[K, V] {"type":"object","additionalProperties":…}
(A, B[, C[, D]]) {"type":"array","prefixItems":[…]} (positional, for composite keys)

For example, User above derives roughly:

{
  "type": "object",
  "properties": {
    "name":     { "type": "string" },
    "age":      { "type": "integer" },
    "nickname": { "type": "string" }
  },
  "required": ["name", "age"]
}

Recursive models (e.g. case class Tree(children: Seq[Tree])) would make the inductive derivation diverge at compile time. Declare an explicit given LMDBSchema[T] for such types.


SchemaArtifact

The persisted artifact is a small sum type, stored next to the collection metadata:

Variant Carries
JsonSchema(schema: JValue) the structural shape derived from a value type
KeySchema(keyId: String) the key codec’s stable identity
ProtobufSchema(proto: String) a .proto source for Protobuf-backed codecs
OpaqueSchema(hint: String) fallback for a type that exposes no shape

Every artifact has a stable fingerprint — a SHA-256 of its canonical JSON form — used for structural drift detection (the fixed field order makes it stable across JVMs and platforms).

val art = LMDBSchema[User].artifact     // SchemaArtifact.JsonSchema(...)
val fp  = art.fingerprint               // "…64-hex-chars…"

Custom schema for a custom codec

If you wire your own LMDBCodec[T], declare a matching schema given so the collection stays self-describing:

given LMDBSchema[MyRecord] =
  LMDBSchema.from(SchemaArtifact.ProtobufSchema(MyRecord.protoSource))

// or, to keep it explicit but opaque:
given LMDBSchema[MyRecord] =
  LMDBSchema.from(SchemaArtifact.OpaqueSchema("MyRecord"))

How the SQL layer uses it

The SQL DESCRIBE / \d reads these artifacts directly: the key row comes from the KeySchema id, and the value columns and their types come from the JsonSchema properties. That is why DESCRIBE reports real columns for a derived value type and only _value for an opaque one.


ZIO-LMDB — Copyright © 2026 David Crosson. Licensed under the Apache 2.0 License.

This site uses Just the Docs, a documentation theme for Jekyll.