There’s a specific kind of error that annotate doesn’t cover: a missing key. When a required key is absent from the input object, the error isn’t about the value being wrong (there is no value), it’s about the key itself being missing.
You customize this with annotateKey, not annotate:
import { Schema } from "effect";
const LoginForm = Schema.Struct({ username: Schema.String.annotateKey({ messageMissingKey: "Username is required", }), password: Schema.String.annotateKey({ messageMissingKey: "Password is required", }),});
const result = Schema.decodeUnknownExit(LoginForm)({});console.log(String(result));Output:
Failure(Cause([Fail(SchemaError(Username is required at ["username"]))]))Without the messageMissingKey annotation, the error would just say “Missing key”, which is technically correct, but not helpful for a user staring at a login form.
annotateKey vs annotate
This is a subtle but important distinction:
annotatetargets the value. It fires when a value is present but has the wrong type. If someone types “abc” into an age field,annotate({ message: "Age must be a number" })provides the error.annotateKeytargets the key. It fires when the key is entirely absent from the input. If someone leaves the age field blank and the form omits the key entirely,annotateKey({ messageMissingKey: "Age is required" })provides the error.
You’ll often want both on the same field:
import { Schema } from "effect";
const schema = Schema.Struct({ age: Schema.Number.annotate({ message: "Age must be a number" }) // value is wrong type .annotateKey({ messageMissingKey: "Age is required" }), // key is missing});Now age has a custom message for both error scenarios: missing key and wrong type.