Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/schema-codec-narrowing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"effect": patch
---

Schema: add `Schema.Decoder` and `Schema.Encoder`, and accept simpler schema types in APIs that only decode, only encode, or only need the basic schema shape, closes #2536
2 changes: 1 addition & 1 deletion packages/effect/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ const recur: (
* @category schemas
* @since 4.0.0
*/
export function schema<T, E>(codec: Schema.Codec<T, E>, path?: string | ConfigProvider.Path): Config<T> {
export function schema<T>(codec: Schema.ConstraintCodec<T, unknown>, path?: string | ConfigProvider.Path): Config<T> {
const codecStringTree = Schema.toCodecStringTree(codec)
const decodeUnknownEffect = SchemaParser.decodeUnknownEffect(codecStringTree)
const codecStringTreeEncoded = SchemaAST.toEncoded(codecStringTree.ast)
Expand Down
113 changes: 80 additions & 33 deletions packages/effect/src/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -888,31 +888,6 @@ export declare namespace Codec {
export type EncodingServices<S> = S extends { readonly "EncodingServices": infer R } ? R : never
}

/**
* A schema that additionally supports optic (lens/prism) operations.
*
* **Details**
*
* `Optic<T, Iso>` extends {@link Schema}`<T>` with an `Iso` type that
* describes the isomorphic counterpart used by the optic layer. Crucially,
* decoding and encoding require *no* Effect services (`DecodingServices` and
* `EncodingServices` are both `never`), which means the optic can operate
* purely without an Effect runtime.
*
* Most primitive schemas (e.g. `Schema.String`, `Schema.Number`) implement
* `Optic` automatically. You normally interact with this interface through
* {@link Optic_} utilities rather than constructing it directly.
*
* @category models
* @since 4.0.0
*/
export interface Optic<out T, out Iso> extends Schema<T> {
readonly "Iso": Iso
readonly "DecodingServices": never
readonly "EncodingServices": never
readonly "Rebuild": Optic<T, Iso>
}

/**
* A schema that tracks the decoded type `T`, the encoded type `E`, and the
* Effect services required during decoding (`RD`) and encoding (`RE`).
Expand All @@ -924,8 +899,8 @@ export interface Optic<out T, out Iso> extends Schema<T> {
* Most concrete schemas produced by this module implement `Codec`.
*
* For APIs that only need one direction, prefer the narrower views:
* - {@link ConstraintDecoder}`<T, RD>` — decode-only
* - {@link ConstraintEncoder}`<E, RE>` — encode-only
* - {@link Decoder}`<T, RD>` — decode-only
* - {@link Encoder}`<E, RE>` — encode-only
* - {@link Schema}`<T>` — type-only (no encoded representation)
*
* **Example** (Accepting a codec that decodes to `number` from `string`)
Expand Down Expand Up @@ -953,6 +928,52 @@ export interface Codec<out T, out E = T, out RD = never, out RE = never> extends
readonly "Rebuild": Codec<T, E, RD, RE>
}

/**
* A schema that tracks the decoded type `T` and the Effect services required
* during decoding (`RD`).
*
* **When to use**
*
* Use when you need to preserve a schema's decoded type and decoding service
* requirements, but do not need to constrain its encoded representation or
* encoding services.
*
* @see {@link Codec} for preserving both decoded and encoded type information.
* @see {@link Encoder} for the encode-only view.
*
* @category models
* @since 4.0.0
*/
export interface Decoder<out T, out RD = never> extends Schema<T> {
readonly "Encoded": unknown
readonly "DecodingServices": RD
readonly "EncodingServices": unknown
readonly "Rebuild": Decoder<T, RD>
}

/**
* A schema that tracks the encoded type `E` and the Effect services required
* during encoding (`RE`).
*
* **When to use**
*
* Use when you need to preserve a schema's encoded type and encoding service
* requirements, but do not need to constrain its decoded representation or
* decoding services.
*
* @see {@link Codec} for preserving both decoded and encoded type information.
* @see {@link Decoder} for the decode-only view.
*
* @category models
* @since 4.0.0
*/
export interface Encoder<out E, out RE = never> extends Schema<unknown> {
readonly "Encoded": E
readonly "DecodingServices": unknown
readonly "EncodingServices": RE
readonly "Rebuild": Encoder<E, RE>
}

/**
* Returns a codec widened to the full {@link Codec} interface, prompting
* TypeScript to infer all four type parameters (`T`, `E`, `RD`, `RE`).
Expand Down Expand Up @@ -982,6 +1003,31 @@ export function revealCodec<T, E, RD, RE>(codec: Codec<T, E, RD, RE>) {
return codec
}

/**
* A schema that additionally supports optic (lens/prism) operations.
*
* **Details**
*
* `Optic<T, Iso>` extends {@link Schema}`<T>` with an `Iso` type that
* describes the isomorphic counterpart used by the optic layer. Crucially,
* decoding and encoding require *no* Effect services (`DecodingServices` and
* `EncodingServices` are both `never`), which means the optic can operate
* purely without an Effect runtime.
*
* Most primitive schemas (e.g. `Schema.String`, `Schema.Number`) implement
* `Optic` automatically. You normally interact with this interface through
* {@link Optic_} utilities rather than constructing it directly.
*
* @category models
* @since 4.0.0
*/
export interface Optic<out T, out Iso> extends Schema<T> {
readonly "Iso": Iso
readonly "DecodingServices": never
readonly "EncodingServices": never
readonly "Rebuild": Optic<T, Iso>
}

export {
/**
* Error thrown (or returned as the error channel value) when schema decoding
Expand Down Expand Up @@ -2757,7 +2803,7 @@ export declare namespace TemplateLiteralParser {
*/
export type Type<Parts> = Parts extends readonly [infer Head, ...infer Tail] ? readonly [
Head extends TemplateLiteral.LiteralPart ? Head :
Head extends Codec<infer T, unknown, unknown, unknown> ? T
Head extends ConstraintDecoder<infer T, unknown> ? T
: never,
...Type<Tail>
]
Expand Down Expand Up @@ -12612,7 +12658,8 @@ function getClassSchemaFactory<S extends Constraint>(
{
identifier,
[SchemaAST.ClassTypeId]: ([from]: readonly [SchemaAST.AST]) => new SchemaAST.Link(from, transformation),
toCodec: ([from]: readonly [Codec<S["Encoded"]>]) => new SchemaAST.Link(from.ast, transformation),
toCodec: ([from]: readonly [ConstraintCodec<S["Encoded"], S["Encoded"]>]) =>
new SchemaAST.Link(from.ast, transformation),
toArbitrary: ([from]: readonly [Annotations.ToArbitrary.TypeParameter<S["Type"]>]) => () => ({
arbitrary: from.arbitrary.map((args: S["Type"]) => new self(args)),
terminal: from.terminal?.map((args: S["Type"]) => new self(args))
Expand Down Expand Up @@ -13594,8 +13641,8 @@ type XmlEncoderOptions = {
* @category Canonical Codecs
* @since 4.0.0
*/
export function toEncoderXml<T, E, RD, RE>(
codec: Codec<T, E, RD, RE>,
export function toEncoderXml<T, RE>(
codec: ConstraintCodec<T, unknown, unknown, RE>,
options?: XmlEncoderOptions
) {
const rootName = InternalAnnotations.resolveIdentifier(codec.ast) ?? InternalAnnotations.resolveTitle(codec.ast)
Expand Down Expand Up @@ -13930,7 +13977,7 @@ export interface overrideToCodecIso<S extends Constraint, Iso> extends
* @since 4.0.0
*/
export function overrideToCodecIso<S extends Constraint, Iso>(
to: Codec<Iso>,
to: ConstraintCodec<Iso>,
transformation: {
readonly decode: SchemaGetter.Getter<S["Type"], Iso>
readonly encode: SchemaGetter.Getter<Iso, S["Type"]>
Expand Down Expand Up @@ -13958,7 +14005,7 @@ export function overrideToCodecIso<S extends Constraint, Iso>(
* @category converting
* @since 4.0.0
*/
export function toDifferJsonPatch<T, E>(schema: Codec<T, E>): Differ<T, JsonPatch.JsonPatch> {
export function toDifferJsonPatch<T>(schema: ConstraintCodec<T, unknown>): Differ<T, JsonPatch.JsonPatch> {
const serializer = toCodecJson(schema)
const get = SchemaParser.encodeSync(serializer)
const set = SchemaParser.decodeSync(serializer)
Expand Down
4 changes: 2 additions & 2 deletions packages/effect/src/SchemaParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,8 @@ export function make<S extends Schema.Constraint>(schema: S) {
* @category Asserting
* @since 3.10.0
*/
export function is<T>(schema: Schema.Schema<T>): <I>(input: I) => input is I & T {
return _is<T>(schema.ast)
export function is<S extends Schema.Constraint>(schema: S): <I>(input: I) => input is I & S["Type"] {
return _is<S["Type"]>(schema.ast)
}

/** @internal */
Expand Down
4 changes: 2 additions & 2 deletions packages/effect/src/testing/TestSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export class Asserts<S extends Schema.Constraint> {
*
* @see {@link arbitrary} for checking that generated values satisfy the schema
*/
verifyLosslessTransformation<S extends Schema.Codec<unknown, unknown>>(this: Asserts<S>, options?: {
verifyLosslessTransformation<S extends Schema.ConstraintCodec<unknown, unknown>>(this: Asserts<S>, options?: {
readonly params?: FastCheck.Parameters<[S["Type"]]>
}) {
const decodeUnknownEffect = SchemaParser.decodeUnknownEffect(this.schema)
Expand Down Expand Up @@ -272,7 +272,7 @@ export class Asserts<S extends Schema.Constraint> {
*
* @see {@link verifyLosslessTransformation} for property-based round-trip checks
*/
arbitrary<S extends Schema.Codec<unknown, unknown, never, unknown>>(this: Asserts<S>) {
arbitrary<S extends Schema.ConstraintCodec<unknown, unknown>>(this: Asserts<S>) {
const schema = this.schema
return {
verifyGeneration(options?: {
Expand Down
2 changes: 1 addition & 1 deletion packages/effect/src/unstable/ai/Chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ export interface Service {
*/
readonly generateObject: <
ObjectEncoded extends Record<string, any>,
ObjectSchema extends Schema.Codec<unknown, ObjectEncoded, unknown, unknown>,
ObjectSchema extends Schema.Encoder<ObjectEncoded, unknown>,
Options extends NoExcessProperties<LanguageModel.GenerateObjectOptions<any, ObjectSchema>, Options>
>(
options: Options & LanguageModel.GenerateObjectOptions<LanguageModel.ExtractTools<Options>, ObjectSchema>
Expand Down
6 changes: 3 additions & 3 deletions packages/effect/src/unstable/ai/LanguageModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export interface Service {
*/
readonly generateObject: <
ObjectEncoded extends Record<string, any>,
StructuredOutputSchema extends Schema.Codec<unknown, ObjectEncoded, unknown, unknown>,
StructuredOutputSchema extends Schema.Encoder<ObjectEncoded, unknown>,
Options extends NoExcessProperties<
GenerateObjectOptions<any, StructuredOutputSchema>,
Options
Expand Down Expand Up @@ -835,7 +835,7 @@ export const make: (params: {

const generateObject = <
ObjectEncoded extends Record<string, any>,
StructuredOutputSchema extends Schema.Codec<unknown, ObjectEncoded, unknown, unknown>,
StructuredOutputSchema extends Schema.Encoder<ObjectEncoded, unknown>,
Options extends NoExcessProperties<
GenerateObjectOptions<any, StructuredOutputSchema>,
Options
Expand Down Expand Up @@ -1691,7 +1691,7 @@ export const generateText: {
*/
export const generateObject = <
ObjectEncoded extends Record<string, any>,
StructuredOutputSchema extends Schema.Codec<unknown, ObjectEncoded, unknown, unknown>,
StructuredOutputSchema extends Schema.Encoder<ObjectEncoded, unknown>,
Options extends NoExcessProperties<
GenerateObjectOptions<any, StructuredOutputSchema>,
Options
Expand Down
10 changes: 7 additions & 3 deletions packages/effect/src/unstable/cli/Argument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,9 +590,13 @@ export const between: {
* @since 4.0.0
*/
export const withSchema: {
<A, B>(schema: Schema.Codec<B, A, Environment, Environment>): (self: Argument<A>) => Argument<B>
<A, B>(self: Argument<A>, schema: Schema.Codec<B, A, Environment, Environment>): Argument<B>
} = dual(2, <A, B>(self: Argument<A>, schema: Schema.Codec<B, A>) => Param.withSchema(self, schema))
<A, B>(schema: Schema.ConstraintCodec<B, A, Environment, unknown>): (self: Argument<A>) => Argument<B>
<A, B>(self: Argument<A>, schema: Schema.ConstraintCodec<B, A, Environment, unknown>): Argument<B>
} = dual(
2,
<A, B>(self: Argument<A>, schema: Schema.ConstraintCodec<B, A, Environment, unknown>): Argument<B> =>
Param.withSchema(self, schema)
)

/**
* Creates a positional choice argument with custom value mapping.
Expand Down
9 changes: 6 additions & 3 deletions packages/effect/src/unstable/cli/Flag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,9 @@ export const orElseResult: {
* @since 4.0.0
*/
export const withSchema: {
<A, B>(schema: Schema.Codec<B, A, Environment, Environment>): (self: Flag<A>) => Flag<B>
<A, B>(self: Flag<A>, schema: Schema.Codec<B, A, Environment, Environment>): Flag<B>
} = dual(2, <A, B>(self: Flag<A>, schema: Schema.Codec<B, A>) => Param.withSchema(self, schema))
<A, B>(schema: Schema.ConstraintCodec<B, A, Environment, unknown>): (self: Flag<A>) => Flag<B>
<A, B>(self: Flag<A>, schema: Schema.ConstraintCodec<B, A, Environment, unknown>): Flag<B>
} = dual(
2,
<A, B>(self: Flag<A>, schema: Schema.ConstraintCodec<B, A, Environment, unknown>) => Param.withSchema(self, schema)
)
6 changes: 3 additions & 3 deletions packages/effect/src/unstable/cli/Param.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1722,17 +1722,17 @@ export const withMetavar: {
*/
export const withSchema: {
<A, B>(
schema: Schema.Codec<B, A, Environment, Environment>
schema: Schema.ConstraintCodec<B, A, Environment, unknown>
): <Kind extends ParamKind>(
self: Param<Kind, A>
) => Param<Kind, B>
<Kind extends ParamKind, A, B>(
self: Param<Kind, A>,
schema: Schema.Codec<B, A, Environment, Environment>
schema: Schema.ConstraintCodec<B, A, Environment, unknown>
): Param<Kind, B>
} = dual(2, <Kind extends ParamKind, A, B>(
self: Param<Kind, A>,
schema: Schema.Codec<B, A>
schema: Schema.ConstraintCodec<B, A, Environment, unknown>
) => {
const decodeParam = Schema.decodeUnknownEffect(schema)
return mapEffect(self, (value) =>
Expand Down
4 changes: 2 additions & 2 deletions packages/effect/src/unstable/cli/Primitive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ const makePrimitive = <A>(
parse
})

const makeSchemaPrimitive = <T, E>(
const makeSchemaPrimitive = <T>(
tag: string,
schema: Schema.Codec<T, E, Environment, Environment>
schema: Schema.ConstraintDecoder<T, Environment>
): Primitive<T> => {
const toCodecStringTree = Schema.toCodecStringTree(schema)
const decode = Schema.decodeUnknownEffect(toCodecStringTree)
Expand Down
6 changes: 3 additions & 3 deletions packages/effect/src/unstable/encoding/Sse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,15 @@ export const decode = <IE, Done>(): Channel.Channel<
* @since 4.0.0
*/
export interface EventCodec extends
Schema.Codec<
Schema.ConstraintCodec<
any,
{
readonly id?: string | undefined
readonly event?: string | undefined
readonly data: string
},
any,
any
unknown,
unknown
>
{}

Expand Down
5 changes: 2 additions & 3 deletions packages/effect/src/unstable/http/HttpClientResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,9 @@ export const schemaJson = <
readonly headers?: Readonly<Record<string, string | undefined>> | undefined
readonly body?: unknown
},
RD,
RE
RD
>(
schema: Schema.Codec<A, I, RD, RE>,
schema: Schema.ConstraintCodec<A, I, RD, unknown>,
options?: ParseOptions | undefined
) => {
const decode = Schema.decodeEffect(Schema.toCodecJson(schema).annotate({ options }))
Expand Down
9 changes: 4 additions & 5 deletions packages/effect/src/unstable/http/HttpIncomingMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,9 @@ export const schemaBodyJson = <S extends Schema.Constraint>(schema: S, options?:
export const schemaBodyUrlParams = <
A,
I extends Readonly<Record<string, string | ReadonlyArray<string> | undefined>>,
RD,
RE
RD
>(
schema: Schema.Codec<A, I, RD, RE>,
schema: Schema.ConstraintCodec<A, I, RD, unknown>,
options?: ParseOptions | undefined
) => {
const decode = UrlParams.schemaRecord.pipe(
Expand All @@ -98,8 +97,8 @@ export const schemaBodyUrlParams = <
* @category schemas
* @since 4.0.0
*/
export const schemaHeaders = <A, I extends Readonly<Record<string, string | undefined>>, RD, RE>(
schema: Schema.Codec<A, I, RD, RE>,
export const schemaHeaders = <A, I extends Readonly<Record<string, string | undefined>>, RD>(
schema: Schema.ConstraintCodec<A, I, RD, unknown>,
options?: ParseOptions | undefined
) => {
const decode = Schema.decodeUnknownEffect(schema)
Expand Down
Loading
Loading