Effect
Generate Effect schemas from OpenAPI
Generate Effect Schema schemas from your OpenAPI specification for runtime validation.
Configuration
Set the client option to effect:
import { defineConfig } from 'orval';
export default defineConfig({
petstore: {
output: {
client: 'effect',
mode: 'single',
target: './src/api/schemas',
},
input: {
target: './petstore.yaml',
},
},
});Generated Output
Orval generates an Effect schema for each model in your OpenAPI specification:
import { Schema as S } from 'effect';
export const CreatePetsBody = S.Struct({
id: S.Number,
name: S.String,
tag: S.optional(S.String),
});Effect Version
Orval reads the effect version installed in your project and emits Effect 3 or Effect 4 Schema syntax to match. When no effect package is found, it emits Effect 4. Set override.effect.version to pin the target.
Options
Options live under override.effect:
strict- per location (param,query,header,body,response), fail on extra fields in object schemas. Allfalseby default.generate- per location, choose which schemas are generated. Alltrueby default.useBrandedTypes- appendS.brand()to the generated schemas.falseby default.exactOptional- emit optional properties for projects withexactOptionalPropertyTypes.falseby default. Can also be set per operation or tag.version- pin the Effect major version (3,4or'auto') instead of reading it from the installed package. Output-wide.generateEachHttpStatus- emit a schema per response status code.falseby default.generateReusableSchemas- emit each component schema an operation reaches once and reference it, instead of inlining every$ref.falseby default. See Reusable Schemas.
Reusable Schemas
By default every $ref is inlined, which makes the output large for a spec with many shared components. With generateReusableSchemas, each component schema an operation reaches becomes one exported constant:
export default defineConfig({
petstore: {
output: {
client: 'effect',
target: './src/api/schemas.ts',
override: { effect: { generateReusableSchemas: true } },
},
input: { target: './petstore.yaml' },
},
});export type Pet = {
readonly name: string;
readonly friends?: ReadonlyArray<Pet> | undefined;
};
export type PetEncoded = {
readonly name: string;
readonly friends?: ReadonlyArray<PetEncoded> | undefined;
};
export const Pet = S.Struct({
name: S.String,
friends: S.optional(
S.Array(S.suspend((): S.Codec<Pet, PetEncoded> => Pet)),
),
});
export const ListPetsResponseItem = Pet;
export const ListPetsResponse = S.Array(ListPetsResponseItem);The constants go next to the operations: into the target in single mode, into a *.schemas.ts file in the other modes. A reference to a constant on a cycle reads through S.suspend, typed with S.Schema on Effect 3 and S.Codec on Effect 4.
Each constant on a cycle also gets two exported types: Pet for what it decodes to and PetEncoded for what it accepts. The S.suspend getter uses them, so the recursive field keeps its type. When another schema already has the name PetEncoded, the encoded type becomes PetEncoded2. A field converted by a formatType is typed from its effect.schema. An expression that is not a plain identifier is declared as a constant (such as petSlugFormat) for that.
Limitations
- With
client: 'effect',generateReusableSchemasandoutput.schemascan't be set together: generation fails with an error. - A
$refis still inlined where a constant would not behave the same, such as form bodies, parameters,allOfmembers, an array body or response (generated from its items, which stay references), schemas with adefault, and members of aoneOforanyOfwith sibling keywords such asproperties,typeorrequired. Effect mergesallOfmembers by their fields, so anallOfof references stays inlined. - A
$refto a schema that isreadOnlyorwriteOnlyis always inlined, and so is one that holds such a property when it is read in a request body. - A location with another
exactOptionalthan the output inlines its schemas. - Only the components the generated operations use, and those they read in turn, are generated. A component only an error response reads is not, unless
generateEachHttpStatusis on, and neither is one only an unsupported media type reads. One of them that can't be generated, such as anallOfof a schema with itself, stops generation with an error that names it. - A component named
Date,RegExp,File,S,EffectorReadonlyArray, or like a name aformatTypeeffect.schemaorschemaImportreads, gets aSchemasuffix. Two components with the same name stop generation with an error, and so does a constant hoisted from a component or an operation (such asPetMin) that has the name of another component. - A constant of an operation (such as
ListPetsResponseorCreatePetsBody) that has the name of a component gets aSchemasuffix, even when no constant is written for that component. Code that imports the name it has without the option stops compiling. - A cycle closed by a
$refthat can't stay a reference, such as one withreadOnlyorwriteOnlynext to it, is validated as unknown where it closes, with a warning, as it is without the option. A constant is expanded from itself, so a schema reached through an alias is validated one level less than it is without the option. - On Effect 4, TypeScript can report
Type instantiation is excessively deepfor a constant on a cycle that reads a chain of about 16 or more nested components outside any cycle. Leave the option off for such a spec.
Usage
Decoding Data
import { Schema as S } from 'effect';
import { CreatePetsBody } from './src/api/schemas';
const pet = { id: 1, name: 'Buddy', tag: 'dog' };
const parsedPet = S.decodeUnknownSync(CreatePetsBody)(pet);
// => { id: 1, name: "Buddy", tag: "dog" }Type Inference
import { Schema as S } from 'effect';
import { CreatePetsBody } from './src/api/schemas';
type Pet = S.Schema.Type<typeof CreatePetsBody>;
const pet: Pet = { id: 1, name: 'Buddy', tag: 'dog' };Safe Decoding
import { Exit, Schema as S } from 'effect';
const result = S.decodeUnknownExit(CreatePetsBody)(unknownData);
if (Exit.isSuccess(result)) {
console.log(result.value);
} else {
console.error(result.cause);
}With Effect 3, use S.decodeUnknownEither and Either.isRight instead.
Combining with HTTP Clients
See the Client with Effect guide for using Effect schemas with React Query, SWR, or other HTTP clients.