Last active
September 4, 2025 02:33
-
-
Save sillvva/24e1ccd54c87ffa6dbdf563f7cc0cebb to your computer and use it in GitHub Desktop.
StandardSchemaV1 Codec
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import type { StandardSchemaV1 } from "@standard-schema/spec"; | |
| type Context<TType> = { | |
| parsed: TType; | |
| issues: StandardSchemaV1.Issue[]; | |
| }; | |
| // Transform functions interface | |
| interface CodecTransforms<TInputIn, TInputOut, TOutputIn, TOutputOut> { | |
| decode: (input: TInputIn, context?: Context<TInputOut>) => TOutputIn; // forward transform (input -> output) | |
| encode: (output: TOutputIn, context?: Context<TOutputOut>) => TInputIn; // reverse transform (output -> input) | |
| } | |
| // Main codec utility function | |
| export function codec<TInputIn, TInputOut, TOutputIn, TOutputOut>( | |
| inputSchema: StandardSchemaV1<TInputIn, TInputOut>, | |
| outputSchema: StandardSchemaV1<TOutputIn, TOutputOut>, | |
| transforms: CodecTransforms<TInputIn, TInputOut, TOutputIn, TOutputOut> | |
| ) { | |
| const safeDecode = (value: TInputIn): StandardSchemaV1.Result<TOutputOut> => { | |
| const inputResult = inputSchema["~standard"].validate(value); | |
| if (inputResult instanceof Promise) | |
| throw new Error(`${inputSchema["~standard"].vendor} validator is async. Use decodeAsync or safeDecodeAsync instead.`); | |
| if (inputResult.issues) return inputResult; | |
| const context: Context<TInputOut> = { parsed: inputResult.value, issues: [] }; | |
| const transformed = transforms.decode(value, context); | |
| if (context.issues.length > 0) return { issues: context.issues }; | |
| return outputSchema["~standard"].validate(transformed) as StandardSchemaV1.Result<TOutputOut>; | |
| }; | |
| const safeEncode = (value: TOutputIn): StandardSchemaV1.Result<TInputOut> => { | |
| const outputResult = outputSchema["~standard"].validate(value); | |
| if (outputResult instanceof Promise) | |
| throw new Error(`${outputSchema["~standard"].vendor} validator is async. Use encodeAsync or safeEncodeAsync instead.`); | |
| if (outputResult.issues) return outputResult; | |
| const context: Context<TOutputOut> = { parsed: outputResult.value, issues: [] }; | |
| const transformed = transforms.encode(value, context); | |
| if (context.issues.length > 0) return { issues: context.issues }; | |
| return inputSchema["~standard"].validate(transformed) as StandardSchemaV1.Result<TInputOut>; | |
| }; | |
| const decode = (value: TInputIn) => { | |
| const result = safeDecode(value); | |
| if (result.issues) throw new ParseError(result.issues); | |
| return result.value; | |
| }; | |
| const encode = (value: TOutputIn) => { | |
| const result = safeEncode(value); | |
| if (result.issues) throw new ParseError(result.issues); | |
| return result.value; | |
| }; | |
| const safeDecodeAsync = async (value: TInputIn): Promise<StandardSchemaV1.Result<TOutputOut>> => { | |
| const inputResult = await inputSchema["~standard"].validate(value); | |
| if (inputResult.issues) return inputResult; | |
| const context: Context<TInputOut> = { parsed: inputResult.value, issues: [] }; | |
| const transformed = transforms.decode(value, context); | |
| if (context.issues.length > 0) return { issues: context.issues }; | |
| return await outputSchema["~standard"].validate(transformed); | |
| }; | |
| const safeEncodeAsync = async (value: TOutputIn): Promise<StandardSchemaV1.Result<TInputOut>> => { | |
| const outputResult = await outputSchema["~standard"].validate(value); | |
| if (outputResult.issues) return outputResult; | |
| const context: Context<TOutputOut> = { parsed: outputResult.value, issues: [] }; | |
| const transformed = transforms.encode(value, context); | |
| if (context.issues.length > 0) return { issues: context.issues }; | |
| return await inputSchema["~standard"].validate(transformed); | |
| }; | |
| const decodeAsync = async (value: TInputIn) => { | |
| const result = await safeDecodeAsync(value); | |
| if (result.issues) throw new ParseError(result.issues); | |
| return result.value; | |
| }; | |
| const encodeAsync = async (value: TOutputIn) => { | |
| const result = await safeEncodeAsync(value); | |
| if (result.issues) throw new ParseError(result.issues); | |
| return result.value; | |
| }; | |
| return { | |
| safeDecode, | |
| safeEncode, | |
| decode, | |
| encode, | |
| safeDecodeAsync, | |
| safeEncodeAsync, | |
| decodeAsync, | |
| encodeAsync | |
| }; | |
| } | |
| export class ParseError extends Error { | |
| override name = "ParseError"; | |
| constructor(public issues: readonly StandardSchemaV1.Issue[]) { | |
| let numIssues = 0; | |
| const message = issues.flatMap((issue) => { | |
| const lines = [`- ${issue.message}`]; | |
| if (issue.path) { | |
| lines.push( | |
| ` Path: ${issue.path?.reduce<string>((acc, p, idx) => { | |
| const key = typeof p === "object" ? p.key : p; | |
| if (typeof key === "number") return `${acc}[${key}]`; | |
| return idx === 0 ? `${String(key)}` : `${acc}.${String(key)}`; | |
| }, "")}` | |
| ); | |
| } | |
| numIssues++; | |
| return lines; | |
| }); | |
| message.unshift(`${numIssues} issue${numIssues === 1 ? "" : "s"} detected:`); | |
| super(message.join("\n")); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment