# Models and variant schemas

A single domain object often needs several different schema shapes. The row you
read from the database is not the row you insert (the database generates the id
and timestamps), and neither matches the JSON you accept on an HTTP `POST` or the
JSON you serialize on a response (you must never leak a password hash). Writing
those shapes by hand means keeping four or five near-identical structs in sync.

`Model` solves this by keeping **one field declaration as the source of truth** and
deriving the variants from it: `select`, `insert`, `update` (database-facing) plus
`json`, `jsonCreate`, `jsonUpdate` (API-facing). Each variant is a real
[`Schema.Struct`](https://effect.plants.sh/schema/structs-and-records/), so you decode, encode, and compose
with it like any other schema. `VariantSchema` is the lower-level engine that
powers `Model` — use it directly when you want a *different* set of variants (for
example `public` / `private`).
**Unstable module:** `Model` and `VariantSchema` live under `effect/unstable/schema`. Unstable
  modules are production-usable but their API may change between minor releases.

```ts
import { Model } from "effect/unstable/schema"
```

## The common case: `Model.Class`

Define a class once. Plain schemas (like `Schema.String`) appear in every variant;
the `Model.*` field helpers opt a property into only the variants where it makes
sense.

```ts
import { Schema } from "effect"
import { Model } from "effect/unstable/schema"

// Branded id so a GroupId can't be confused with another numeric id.
export const GroupId = Schema.Number.pipe(Schema.brand("GroupId"))

export class Group extends Model.Class<Group>("Group")({
  // Database generates the id: present in select/json, omitted from insert/update.
  id: Model.GeneratedByDb(GroupId),
  // Plain schema -> present in ALL variants.
  name: Schema.String,
  // Stamped with the current time on insert; omitted from update.
  createdAt: Model.DateTimeInsertFromDate,
  // Stamped with the current time on insert AND update.
  updatedAt: Model.DateTimeUpdateFromDate
}) {}

// The class itself is the `select` schema (the full row):
Group
Group.fields // the original field declaration

// The generated variants, each a Schema.Struct in its own right:
Group.insert // { name, createdAt, updatedAt } — no id, timestamps auto-filled
Group.update // { name, updatedAt } — no id/createdAt, updatedAt auto-filled
Group.json // { id, name, createdAt, updatedAt } as JSON
Group.jsonCreate // { name }
Group.jsonUpdate // { name }
```

Because every variant is a normal schema, you decode/encode at the boundary it
matches:

```ts
import { Schema } from "effect"

// Validate an incoming HTTP POST body against the create variant.
const decodeCreate = Schema.decodeUnknownSync(Group.jsonCreate)
decodeCreate({ name: "Engineers" })
// => { name: "Engineers" }

// Encode a full row to JSON for a response (DateTime -> ISO string).
const toJson = Schema.encodeUnknownSync(Group.json)
```

You can wrap any variant in its own [`Schema.Class`](https://effect.plants.sh/schema/classes/) to attach
methods or getters:

```ts
import { Schema } from "effect"

class GroupJson extends Schema.Class<GroupJson>("GroupJson")(Group.json) {
  get upperName() {
    return this.name.toUpperCase()
  }
}
```
**Variants are projections, not a discriminated union:** A variant is a *subset/transformation* of the same object — switching variants
  never adds a tag. If you build a `Model.Union` whose members must be told apart
  at runtime, include an explicit literal tag field (e.g.
  `_tag: Schema.Literal("Group")`) yourself.

This page documents the **schema mechanics** of `Model` and `VariantSchema`. For
turning a model into a CRUD repository with `insert` / `findById` / `update` and
running migrations, see [SQL models and migrations](https://effect.plants.sh/sql/models-and-migrations/).

## How variants map to your boundaries

| Boundary | Variant | Why |
| --- | --- | --- |
| Read a DB row | `select` (the class itself) | full persisted shape |
| Write a new DB row | `insert` | drops generated columns, fills insert defaults |
| Patch a DB row | `update` | fills update timestamps |
| Serialize a response | `json` | hides sensitive fields, encodes dates as strings |
| Validate a create request | `jsonCreate` | drops server-controlled fields |
| Validate an update request | `jsonUpdate` | drops server-controlled fields |

---

## `Model` reference

### Constructors and variant builders

These are re-exported from `VariantSchema` pre-configured with the six
database/JSON variants, so you rarely call `VariantSchema.make` yourself.

#### `Model.Class`

Defines a model as a schema class. The class is the default (`select`) schema and
exposes the other variants as static properties.

```ts
import { Schema } from "effect"
import { Model } from "effect/unstable/schema"

class User extends Model.Class<User>("User")({
  id: Model.GeneratedByDb(Schema.Number),
  name: Schema.String
}) {}

Schema.decodeUnknownSync(User)({ id: 1, name: "Ada" })
// => User { id: 1, name: "Ada" }
```

#### `Model.Struct`

Like `Model.Class` but produces a plain variant struct (no class identity). Useful
for nesting inside another model or for extracting variants manually.

```ts
import { Schema } from "effect"
import { Model } from "effect/unstable/schema"

const Address = Model.Struct({
  street: Schema.String,
  city: Schema.String
})
Model.extract(Address, "json") // -> Schema.Struct<{ street; city }>
```

#### `Model.Field`

Builds a variant field from a map of `variant -> schema`. A property only appears
in the variants you list. This is the primitive all the helpers below are built on.

```ts
import { Schema } from "effect"
import { Model } from "effect/unstable/schema"

// Appears in select/update/json, but not insert/jsonCreate/jsonUpdate.
const slug = Model.Field({
  select: Schema.String,
  update: Schema.String,
  json: Schema.String
})
```

#### `Model.FieldOnly`

Pipeable helper that puts one schema into *only* the named variants.

```ts
import { Schema } from "effect"
import { Model } from "effect/unstable/schema"

// Only readable, never written.
const computed = Schema.String.pipe(Model.FieldOnly(["select", "json"]))
```

#### `Model.FieldExcept`

The inverse: applies a schema to every variant *except* the named ones.

```ts
import { Schema } from "effect"
import { Model } from "effect/unstable/schema"

// Present everywhere except insert (DB assigns it, but it stays updatable).
const id = Schema.Number.pipe(Model.FieldExcept(["insert"]))
```

#### `Model.fieldEvolve`

Transforms the per-variant schemas of an existing field (or lifts a plain schema
into a field) by variant name.

```ts
import { Schema } from "effect"
import { Model } from "effect/unstable/schema"

// Make the json variants nullable while leaving DB variants untouched.
const note = Schema.String.pipe(
  Model.fieldEvolve({
    json: Schema.NullOr,
    jsonCreate: Schema.NullOr,
    jsonUpdate: Schema.NullOr
  })
)
```

#### `Model.extract`

Pulls a single variant schema out of a model or variant struct as a
`Schema.Struct`.

```ts
import { Schema } from "effect"
import { Model } from "effect/unstable/schema"

class Group extends Model.Class<Group>("Group")({
  id: Model.GeneratedByDb(Schema.Number),
  name: Schema.String
}) {}

Model.extract(Group, "insert") // -> Schema.Struct<{ name: Schema.String }>
```

#### `Model.fields`

Returns the original field declaration object stored on a model/struct (the same
value you passed to `Model.Class`).

```ts
Group.fields
// => { id: GeneratedByDb<...>, name: Schema.String }
```

#### `Model.Union`

Builds a union over the default schemas of several variant structs, and exposes a
per-variant union on each variant name. Add your own literal tag if you need
runtime discrimination.

```ts
import { Schema } from "effect"
import { Model } from "effect/unstable/schema"

const A = Model.Struct({ _tag: Schema.Literal("A"), a: Schema.String })
const B = Model.Struct({ _tag: Schema.Literal("B"), b: Schema.Number })

const AB = Model.Union([A, B])
AB.insert // union of A.insert | B.insert
```

#### `Model.Override`

Brands a value so it is used in place of an `Overrideable` default (such as the
auto-`now()` of the timestamp helpers). Pass it to a constructor when you want to
supply the value explicitly.

```ts
import { DateTime } from "effect"
import { Model } from "effect/unstable/schema"

// Instead of letting createdAt default to the current time:
Model.Override(DateTime.makeUnsafe("2024-01-01T00:00:00Z"))
```

### Generated and lifecycle fields

#### `Model.GeneratedByDb`

A column the **database** generates (auto-increment id, computed column). Present
in `select` and `json` (read-only); omitted from `insert` and `update`.

```ts
import { Schema } from "effect"
import { Model } from "effect/unstable/schema"

const id = Model.GeneratedByDb(Schema.Number)
// insert variant: id is absent; select variant: id is required
```

#### `Model.GeneratedByApp`

A value the **application** generates before insert (e.g. a client-side UUID).
Present in `select`, `insert`, `update`, and `json`; omitted from `jsonCreate` and
`jsonUpdate` (the API client never sets it).

```ts
import { Schema } from "effect"
import { Model } from "effect/unstable/schema"

const ref = Model.GeneratedByApp(Schema.String)
```

#### `Model.Sensitive`

A value present in all database variants but stripped from every JSON variant —
ideal for password hashes and secrets that must never be serialized.

```ts
import { Schema } from "effect"
import { Model } from "effect/unstable/schema"

const passwordHash = Model.Sensitive(Schema.String)
// Present in select/insert/update; absent from json/jsonCreate/jsonUpdate.
```

#### `Model.optionalOption`

A schema (not a field) for a nullable DB column: the encoded side is optional &
nullable, the decoded side is an [`Option`](https://effect.plants.sh/data-types/option/).

```ts
import { Schema } from "effect"
import { Model } from "effect/unstable/schema"

const bio = Model.optionalOption(Schema.String)
Schema.decodeUnknownSync(Schema.Struct({ bio }))({ bio: null })
// => { bio: Option.none() }
Schema.decodeUnknownSync(Schema.Struct({ bio }))({ bio: "hi" })
// => { bio: Option.some("hi") }
```

#### `Model.FieldOption`

Converts a field (or plain schema) so it is optional in every variant: nullable for
the database variants, optional-and-nullable (decoded as `Option`) for the JSON
variants.

```ts
import { Schema } from "effect"
import { Model } from "effect/unstable/schema"

const nickname = Schema.String.pipe(Model.FieldOption)
// DB variants: Option from null; JSON variants: Option from missing/null key.
```

### Encoding helpers

#### `Model.BooleanSqlite`

SQLite has no boolean type. This field stores `0 | 1` in the database variants and
exposes a real `boolean` in the JSON variants.

```ts
import { Schema } from "effect"
import { Model } from "effect/unstable/schema"

class Flag extends Model.Class<Flag>("Flag")({
  active: Model.BooleanSqlite
}) {}

Schema.decodeUnknownSync(Flag)({ active: 1 }) // => Flag { active: true }
Schema.decodeUnknownSync(Flag.json)({ active: true }) // => { active: true }
```

#### `Model.Date`

A `DateTime.Utc` serialized as a `YYYY-MM-DD` string (the time component is
removed on decode).

```ts
import { Schema } from "effect"
import { Model } from "effect/unstable/schema"

const decoded = Schema.decodeUnknownSync(Model.Date)("2024-03-14")
Schema.encodeUnknownSync(Model.Date)(decoded) // => "2024-03-14"
```

#### `Model.JsonFromString`

A field whose value is stored as a JSON *string* in the database variants but uses
the structured schema directly in the JSON variants.

```ts
import { Schema } from "effect"
import { Model } from "effect/unstable/schema"

const Settings = Schema.Struct({ theme: Schema.String })
const settings = Model.JsonFromString(Settings)

class Prefs extends Model.Class<Prefs>("Prefs")({ settings }) {}

// DB variant decodes from a string column:
Schema.decodeUnknownSync(Prefs)({ settings: `{"theme":"dark"}` })
// => Prefs { settings: { theme: "dark" } }
// JSON variant uses the object directly:
Schema.decodeUnknownSync(Prefs.json)({ settings: { theme: "dark" } })
// => { settings: { theme: "dark" } }
```

#### `Model.Uint8Array`

The `Uint8Array` schema (backed by an `ArrayBuffer`) used by the binary UUID
helpers; handy for `BLOB` columns.

```ts
import { Model } from "effect/unstable/schema"

Model.Uint8Array // Schema.instanceOf<Uint8Array<ArrayBuffer>>
```

### DateTime audit-column helpers

These fields fill audit timestamps automatically with the current time. The
`*Insert*` variants set the value only on insert; the `*Update*` variants set it on
both insert and update. The suffix controls the **database** encoding — string,
JavaScript `Date`, or epoch milliseconds (number) — while JSON always uses an ISO
string. In every case `select` and `json` are readable.

```ts
import { Model } from "effect/unstable/schema"

class Doc extends Model.Class<Doc>("Doc")({
  // Set once on insert; omitted from update.
  createdAt: Model.DateTimeInsertFromDate, // DB: Date column
  // Refreshed on insert AND update.
  updatedAt: Model.DateTimeUpdateFromDate // DB: Date column
}) {}
```

Insert-only helpers (omitted from `update`):

- `Model.DateTimeInsert` — DB encodes as a **string**.
- `Model.DateTimeInsertFromDate` — DB encodes as a JavaScript **`Date`**.
- `Model.DateTimeInsertFromNumber` — DB encodes as epoch **milliseconds**.

Insert-and-update helpers (refreshed on every write):

- `Model.DateTimeUpdate` — DB encodes as a **string**.
- `Model.DateTimeUpdateFromDate` — DB encodes as a JavaScript **`Date`**.
- `Model.DateTimeUpdateFromNumber` — DB encodes as epoch **milliseconds**.

Each is built on an `Overrideable` default, so you can supply the value explicitly
with `Model.Override` instead of taking the auto-`now()`:

```ts
import { DateTime } from "effect"
import { Model } from "effect/unstable/schema"

class Doc extends Model.Class<Doc>("Doc")({
  name: Schema.String,
  createdAt: Model.DateTimeInsertFromDate
}) {}

// Auto: createdAt defaults to the current time on insert.
Doc.insert.make({ name: "spec" })
// Explicit: pin createdAt instead of using now().
Doc.insert.make({
  name: "spec",
  createdAt: Model.Override(DateTime.makeUnsafe("2024-01-01T00:00:00Z"))
})
```

#### Overrideable default schemas

These are the underlying `Overrideable` schemas the audit fields are made of. Use
them directly when building a custom field whose constructor should default to the
current time.

- `Model.DateWithNow` — date-only (`YYYY-MM-DD`), defaults to today.
- `Model.DateTimeWithNow` — string-encoded, defaults to now.
- `Model.DateTimeFromDateWithNow` — `Date`-encoded, defaults to now.
- `Model.DateTimeFromNumberWithNow` — millis-encoded, defaults to now.

```ts
import { Model } from "effect/unstable/schema"

class Snapshot extends Model.Class<Snapshot>("Snapshot")({
  // Available in all variants; defaults to now in its constructor.
  takenAt: Model.DateTimeWithNow
}) {}
```

### UUID helpers

Each `*Insert` helper is a field whose `insert` variant carries a constructor
default that generates a UUID, while `select` / `update` / `json` keep the value as
a (branded) read-only column. The `*WithGenerate` functions are the underlying
schemas, so you can attach the generating default to a field of your own.

```ts
import { Schema } from "effect"
import { Model } from "effect/unstable/schema"

const UserId = Schema.String.pipe(Schema.brand("UserId"))

class User extends Model.Class<User>("User")({
  id: Model.UuidV7Insert(UserId),
  name: Schema.String
}) {}

// On insert the id is optional — it is generated if you omit it:
User.insert.make({ name: "Ada" })
```

- `Model.UuidV4Insert(brandedString)` / `Model.UuidV4WithGenerate(brandedString)` —
  string UUID **v4**.
- `Model.UuidV7Insert(brandedString)` / `Model.UuidV7WithGenerate(brandedString)` —
  string UUID **v7** (time-ordered).
- `Model.UuidV4BytesInsert(brandedBytes)` /
  `Model.UuidV4BytesWithGenerate(brandedBytes)` — binary (`Uint8Array`) UUID v4 for
  compact `BLOB` storage.

```ts
import { Schema } from "effect"
import { Model } from "effect/unstable/schema"

// Branded byte id for a compact binary primary key.
const TokenId = Model.Uint8Array.pipe(Schema.brand("TokenId"))
const id = Model.UuidV4BytesInsert(TokenId)
```

---

## `VariantSchema` reference

`VariantSchema` is the engine behind `Model`. Call `VariantSchema.make` with your
own variant names to get a toolkit specialized to them. Reach for it when the
six database/JSON variants don't fit — for example a `public` / `private` API
split.

```ts
import { VariantSchema } from "effect/unstable/schema"
```

### `VariantSchema.make`

Fixes a closed set of variant names and a default variant, returning a toolkit of
`Struct`, `Field`, `FieldOnly`, `FieldExcept`, `fieldEvolve`, `Class`, `Union`, and
`extract` — all typed to your variants.

```ts
import { Schema } from "effect"
import { VariantSchema } from "effect/unstable/schema"

const { Class, Field, extract } = VariantSchema.make({
  variants: ["public", "private"],
  defaultVariant: "public"
})

class Account extends Class<Account>("Account")({
  id: Schema.String, // plain schema -> present in BOTH variants
  // Field opts a property into specific variants:
  email: Field({ private: Schema.String }), // only the private view
  displayName: Field({ public: Schema.String, private: Schema.String })
}) {}

Account // the default (public) schema: { id, displayName }
Account.private // { id, email, displayName }
extract(Account, "private") // -> Schema.Struct of the private variant
```
**Default variant rules:** The default variant is the schema used by generated classes and by
  `Union` members. The other variants are exposed as static properties keyed by
  name. Variants are projections — `make` never injects a discriminator tag.

The toolkit members mirror their `Model` counterparts documented above
(`Class`, `Struct`, `Field`, `FieldOnly`, `FieldExcept`, `fieldEvolve`, `Union`,
`extract`), but typed to *your* variant names rather than the
select/insert/update/json set.

### Types and guards

#### `VariantSchema.TypeId`

The runtime type identifier attached to every variant struct (`"~effect/schema/VariantSchema"`).

```ts
import { VariantSchema } from "effect/unstable/schema"

VariantSchema.TypeId // => "~effect/schema/VariantSchema"
```

#### `VariantSchema.isStruct`

Type guard for a variant schema struct.

```ts
import { Schema } from "effect"
import { VariantSchema } from "effect/unstable/schema"

const { Struct } = VariantSchema.make({
  variants: ["a", "b"],
  defaultVariant: "a"
})

VariantSchema.isStruct(Struct({ x: Schema.String })) // => true
VariantSchema.isStruct(Schema.String) // => false
```

#### `VariantSchema.isField`

Type guard for a variant field (the value `Field({...})` returns).

```ts
import { Schema } from "effect"
import { VariantSchema } from "effect/unstable/schema"

const { Field } = VariantSchema.make({
  variants: ["a", "b"],
  defaultVariant: "a"
})

VariantSchema.isField(Field({ a: Schema.String })) // => true
```

#### `VariantSchema.fields`

Returns the original field declarations stored on a variant struct (same value you
passed in).

```ts
import { Schema } from "effect"
import { VariantSchema } from "effect/unstable/schema"

const { Struct } = VariantSchema.make({
  variants: ["a", "b"],
  defaultVariant: "a"
})

VariantSchema.fields(Struct({ x: Schema.String }))
// => { x: Schema.String }
```

#### `VariantSchema.Override`

The lower-level form of `Model.Override`: brands a value to bypass an
`Overrideable` default.

```ts
import { VariantSchema } from "effect/unstable/schema"

VariantSchema.Override("fixed-value")
```

#### `VariantSchema.Overrideable`

Wraps a schema with an effectful constructor default. The constructor uses the
default unless the supplied value is branded with `Override`. This is exactly how
the `Model.DateTime*WithNow` helpers are built.

```ts
import { DateTime, Effect, Schema } from "effect"
import { VariantSchema } from "effect/unstable/schema"

// A field that defaults to the current time but can be overridden explicitly.
const createdAt = VariantSchema.Overrideable(Schema.DateTimeUtcFromString, {
  defaultValue: DateTime.now
})
```

#### Type-level exports

For library authors writing typed wrappers, the module also exports the
interfaces/namespaces describing these shapes:

- `VariantSchema.Struct<A>` — a variant struct over a field map.
- `VariantSchema.Field<A>` — a variant field over a `variant -> schema` config.
- `VariantSchema.Class` — the schema-class type returned by a toolkit's `Class`.
- `VariantSchema.Union` — the union type returned by a toolkit's `Union`.

---

## Related pages

- [SQL models and migrations](https://effect.plants.sh/sql/models-and-migrations/) — turn a `Model` into a
  repository with `SqlModel.makeRepository` and evolve the schema with the
  Migrator.
- [Schema classes](https://effect.plants.sh/schema/classes/) — wrap a variant in a `Schema.Class` to add
  methods and identity.
- [Structs and records](https://effect.plants.sh/schema/structs-and-records/) — the struct fundamentals
  every variant is built from.