-
-
Notifications
You must be signed in to change notification settings - Fork 527
Description
What version of Effect is running?
3.19.19
What steps can reproduce the bug?
Playground repro
https://effect.website/play/#aeb9f2f87f47
Minimal repro
import { SchemaAST, Schema } from "effect"
// Works
const A = Schema.Struct({
name: Schema.String,
count: Schema.optional(Schema.Number),
})
console.log(SchemaAST.getPropertySignatures(A.ast)) // OK
// Crashes
const B = Schema.Struct({
name: Schema.String,
count: Schema.optionalWith(Schema.Number, { default: () => 10 }),
})
console.log(SchemaAST.getPropertySignatures(B.ast)) // throwsOutput (playground):
[
PropertySignature {
type: StringKeyword { annotations: [Object], _tag: 'StringKeyword' },
annotations: {},
isOptional: false,
name: 'name',
isReadonly: true
},
PropertySignature {
type: Union { types: [Array], annotations: {}, _tag: 'Union' },
annotations: {},
isOptional: true,
name: 'count',
isReadonly: true
}
]
Error: Unsupported schema
schema (Transformation): (Struct (Encoded side) <-> Struct (Type side))
at getPropertyKeyIndexedAccess (.../effect/dist/cjs/SchemaAST.js:1955:9)
at eval (.../effect/dist/cjs/SchemaAST.js:1846:43)
at Object.getPropertySignatures (.../effect/dist/cjs/SchemaAST.js:1846:31)
What is the expected behavior?
getPropertySignatures should return property signatures for the struct, the same way it works when fields use Schema.optional(Schema.Number) (without a default).
What do you see instead?
SchemaAST.getPropertySignatures() throws on any Schema.Struct that contains a field using Schema.optionalWith with options that produce a Transformation AST — this includes { default }, { as: "Option" }, { nullable: true }, and combinations thereof:
Error: Unsupported schema
schema (Transformation): (Struct (Encoded side) <-> Struct (Type side))
This breaks @effect/ai's Tool.getJsonSchema() and LanguageModel.streamText at runtime, since Tool.getJsonSchemaFromSchemaAst calls getPropertySignatures when building tool definitions for the API request.
Additional information
Affected packages: effect (SchemaAST), @effect/ai (Tool)
Why I believe this is a bug
-
The fallback path in
getPropertySignaturesis internally inconsistent. Line 2219 callsgetPropertyKeys(ast)thengetPropertyKeyIndexedAccess(ast, name)on the same AST.getPropertyKeyshandlesTransformation(delegates toast.to), butgetPropertyKeyIndexedAccessdoes not — so the first half succeeds and the second half crashes.getPropertyKeysgainedTransformationsupport in PR #2343 for the benefit ofpick/omit, which handle Transformations directly and never reach the fallback. At that point no caller was passing bare Transformations togetPropertySignatures, so the asymmetry was safe. It became observable when@effect/ai'sTool.getJsonSchemastarted callinggetPropertySignatureson tool parameter schemas that useoptionalWith({ default }). -
Other struct operations handle
Transformation— the internalpickfunction explicitly handlescase "Transformation"forTypeLiteralTransformation. AndJsonSchema.fromASTfully supportsTransformationwithTypeLiteralTransformation, including copying default values from the.toside. -
The existing test suite covers
TypeLiteral,Refinement,Suspend,Union, andClass(via surrogate annotation), but has no test for a bareTransformationfromoptionalWith.
Root cause
Schema.optionalWith(X, { default }) inside a Schema.Struct produces a Transformation AST node (the encoded side has the field as optional; the type side has it as required with the default applied). Unlike Schema.Class or Schema.TaggedRequest, this Transformation does not carry a SurrogateAnnotation, so the surrogate check at the top of getPropertySignatures does not redirect it.
getPropertySignatures falls through its switch to line 2219:
return getPropertyKeys(ast).map((name) => getPropertyKeyIndexedAccess(ast, name))getPropertyKeys handles Transformation (line 2352, delegates to ast.to) and returns the keys. But getPropertyKeyIndexedAccess has no case "Transformation" and throws.
Environment
- effect: 3.19.19
- Runtime: Bun 1.3.5, macOS arm64
Workaround
Replace Schema.optionalWith(X, { default: () => val }) with Schema.optional(X) in tool parameter definitions and apply defaults manually in handler code.