Skip to content
LogoLogo

Schema to Equivalence

Schema-derived equivalence works in VibeLang exactly as in Effect. It compares decoded values using the structure described by a schema, making it useful for domain comparisons and round-trip checks. It does not decode or validate the compared values. Plain builders follow the trusted call rules.

Deriving equality from schemas

// run: Console.log(sameShipment({ reference: "pkg-94", stops: ["east"], extra: 1 }, { reference: "pkg-94", stops: ["east"], extra: 2 })); Console.log(sameShipment({ reference: "pkg-94", stops: ["east"] }, { reference: "pkg-94", stops: ["west"] }))
import { Console } from "effect"
schema Shipment {
  reference: string
  stops: ReadonlyArray<string>
}
 
function sameShipment(left: unknown, right: unknown) {
  const a = Schema.decodeUnknownEffect(Shipment)(left)
  const b = Schema.decodeUnknownEffect(Shipment)(right)
  return try((schema, a, b) => Schema.toEquivalence(schema)(a, b), Shipment, a, b)
}
true
false

toEquivalence derives a comparison for the schema's decoded type. Arrays and nested structs are compared recursively. For a nonempty struct, only the fields described by the schema participate; unmodeled fields do not become extra equality conditions. Decode untrusted inputs before comparing them.

Handling broad schemas and custom declarations

// run: Console.log(sameUnknown({ stops: ["east"] }, { stops: ["east"] })); Console.log(sameRef())
function sameUnknown(left: unknown, right: unknown) {
  return try((left, right) => Schema.toEquivalence(Schema.Unknown)(left, right), left, right)
}
 
class ShipmentRef {
  constructor(readonly id: string, readonly label: string) {}
}
const ShipmentRefSchema = Schema.instanceOf(ShipmentRef, {
  toEquivalence: () => (left, right) => left.id === right.id
})
 
function sameRef() {
  const morning = new ShipmentRef("ref-61", "morning")
  const afternoon = new ShipmentRef("ref-61", "afternoon")
  return try((schema, a, b) => Schema.toEquivalence(schema)(a, b), ShipmentRefSchema, morning, afternoon) // true
}
true
true

Any, Unknown, ObjectKeyword, and the empty struct use Equal.equals, including its structural handling of arrays and objects. Opaque declarations also default to Equal.equals; add a toEquivalence annotation when domain identity follows a particular field. Parameterized declarations receive the derived comparator for each type parameter.

Overriding generated comparison behavior

// run: Console.log(sameIdentity({ reference: "pkg-94", stops: ["east"] }, { reference: "pkg-94", stops: ["west"] })); Console.log(sameIdentity({ reference: "pkg-94", stops: [] }, { reference: "pkg-95", stops: [] }))
const ShipmentIdentity = Shipment.pipe(Schema.overrideToEquivalence(
  () => (left, right) => left.reference === right.reference
))
 
function sameIdentity(left: typeof Shipment.Type, right: typeof Shipment.Type) {
  return try((schema, left, right) => Schema.toEquivalence(schema)(left, right), ShipmentIdentity, left, right)
}
true
false

overrideToEquivalence replaces the comparison derived at that schema node. This comparison intentionally ignores stops. Keep custom comparisons reflexive, symmetric, and transitive; otherwise consumers cannot treat them as an equivalence relation. A comparator callback is synchronous and should not call services or perform effects.


The matching Effect page: effect.website/docs/v4/schema/equivalence