Warning: Effeck is v0. The language will change in major, breaking ways. It is not suitable for production use.

Canonical Codecs

Canonical codec derivation works as in Effect. It preserves a schema's decoded type while choosing a standard external representation for JSON, string trees, or structural updates. Derivation builds a codec; parsing and encoding happen in separate calls. Plain builders follow the trusted call rules.

Deriving standard JSON, string-tree, and identity codecs

const AuditRecord = Schema.Struct({
  created: Schema.Date,
  sequence: Schema.BigInt,
  labels: Schema.Array(Schema.String)
})

function encodeAudit(input: unknown) {
  const value = Schema.decodeUnknownEffect(AuditRecord)(input)
  const jsonCodec = try((schema) => Schema.toCodecJson(schema), AuditRecord)
  return Schema.encodeEffect(jsonCodec)(value)
}
// The JSON representation contains date and bigint strings.

toCodecJson recursively chooses JSON-compatible encodings for domain values, including nested declarations with built-in representations. It preserves explicit encodings already present: a date explicitly encoded as epoch milliseconds remains numeric. Its output is a JSON value, not serialized JSON text; use fromJsonString when text is required.

const Search = Schema.Struct({
  offset: Schema.Int,
  archived: Schema.Boolean,
  labels: Schema.Array(Schema.String)
})

function encodeSearch() {
  const codec = try((schema) => Schema.toCodecStringTree(schema), Search)
  return Schema.encodeEffect(codec)({ offset: 20, archived: false, labels: ["large"] })
}
// { offset: "20", archived: "false", labels: ["large"] }

toCodecStringTree preserves object and array structure and converts leaves to strings. A string tree can also contain undefined, so it is not limited to JSON values. This representation suits configuration, forms, and query parameters.

class Slot extends Schema.Class<Slot>("Slot")({
  row: Schema.Int,
  column: Schema.Int
}) {}

function slotRepresentation(input: unknown) {
  const slot = Schema.decodeUnknownEffect(Slot)(input)
  const codec = try((schema) => Schema.toCodecIso(schema), Slot)
  const structural = Schema.encodeEffect(codec)(slot)
  return Schema.decodeUnknownEffect(codec)(structural)
}

toCodecIso selects the schema's Iso representation, such as plain fields for a class. It supports tools that update plain objects and arrays, then reconstruct the domain value. It is a structural isomorphism codec rather than a guarantee that the original runtime object is retained by identity.

Supplying canonical conversions for custom declarations

class DepotRef {
  constructor(readonly region: string, readonly number: number) {}
}

const DepotRefSchema = Schema.instanceOf(DepotRef, {
  toCodecJson: () => Schema.link<DepotRef>()(
    Schema.Struct({ region: Schema.String, number: Schema.Int }),
    SchemaTransformation.transform({
      decode: (data) => new DepotRef(data.region, data.number),
      encode: (ref) => ({ region: ref.region, number: ref.number })
    })
  )
})

function encodeDepot() {
  const codec = try((schema) => Schema.toCodecStringTree(schema), DepotRefSchema)
  return Schema.encodeEffect(codec)(new DepotRef("nw", 14))
}
// { region: "nw", number: "14" }

Declarations are opaque, so give derivation a structural codec. JSON prefers toCodecJson, then toCodec; StringTree prefers toCodecStringTree, then toCodecJson, then toCodec; Iso prefers toCodecIso, then toCodec. A JSON or StringTree hook can return undefined to say that the declaration is already canonical.

Without a hook, JSON uses an unconstrained JSON fallback and parsing can still fail; StringTree derivation throws; Iso keeps the declaration's own representation. Hooks can use effectful conversions, but cannot add services absent from the resulting codec's service type. The separate Schema.toIso helper consumes its Iso conversion synchronously.

Relating canonical representations to JSON Schema output

function describeDepotJson() {
  return try(Schema.toJsonSchemaDocument(DepotRefSchema))
}

JSON Schema export derives the canonical JSON codec first and describes its encoded side. The custom depot representation therefore appears as an object with a region and number. A custom JSON hook controls both runtime serialization and the exported shape; an opaque declaration without a structural hook yields an unconstrained document.

Upstream page