Codec & Transform
Type-safe data transformation with bidirectional schema documentation.
Overview
Codecs and Transforms provide documented transformations between data formats. Unlike simple functions, they preserve schema metadata on both input and output sides.
The Core Idea: Shared API Documentation
The main purpose of Codecs is to define transformation logic once and share it across boundaries. A single Codec definition serves as both implementation and documentation that any party can execute:
- Client encodes data before sending (e.g.,
AuthInfo->Authorizationheader) - Server decodes data after receiving (e.g.,
Authorizationheader ->AuthInfo) - Server-to-Server can do both directions as needed
This means your API transformation logic is:
- Defined once - No duplicate encode/decode implementations
- Self-documenting - Input and output schemas are always available
- Executable by any party - Client, server, or tooling can run it
- Automatically consistent - Both sides use the exact same transformation
Benefits
- OpenAPI documentation generation from schema metadata
- Type-safe bidirectional transformations (encode/decode)
- Composable transformation pipelines
- Runtime validation of both input and output
GGTransform
A single-direction transformation with input and output schemas.
import {GGTransform} from "@grest-ts/schema"
import {IsString, IsNumber, IsObject} from "@grest-ts/schema"
// Transform string to number
const StringToNumber = new GGTransform(
IsString, // input schema
IsNumber, // output schema
(s) => parseInt(s, 10) // transform function
);
// Use the transform
const result = StringToNumber.encode("42");
if (result.success) {
console.log(result.value); // 42
}Validation Semantics
Transform validates both input and output with different error handling:
- Input validation failure - Returns error result (bad user data)
- Output validation failure - Throws exception (coding error in transform)
// Invalid input - returns error
const result = StringToNumber.encode(123 as any);
// result.success === false, result.issues contains validation errors
// Invalid output - throws (bug in your transform function)
const BadTransform = new GGTransform(
IsString,
IsNumber,
() => "not a number" as any // Bug: returns wrong type
);
BadTransform.encode("test"); // Throws SERVER_ERRORChaining Transforms
const StringToNumber = new GGTransform(IsString, IsNumber, (s) => parseInt(s, 10));
const NumberToBoolean = new GGTransform(IsNumber, IsString, (n) => n > 0 ? "true" : "false");
// Chain: String -> Number -> String("true"/"false")
const StringToBoolString = StringToNumber.transformTo(NumberToBoolean);
const result = StringToBoolString.encode("42");
// result.value === "true"GGCodec
Bidirectional transformation with encode and decode operations.
import {GGCodec, GGTransform} from "@grest-ts/schema"
import {IsString, IsObject} from "@grest-ts/schema"
const AuthInfoSchema = IsObject({
userId: IsString,
token: IsString
});
const AuthHeaderCodec = new GGCodec({
encode: new GGTransform(
AuthInfoSchema,
IsString,
(info) => `Bearer ${info.userId}:${info.token}`
),
decode: new GGTransform(
IsString,
AuthInfoSchema,
(header) => {
const [, payload] = header.split(' ');
const [userId, token] = payload.split(':');
return {userId, token};
}
)
});
// Encode: AuthInfo -> Header string
const encoded = AuthHeaderCodec.encode({userId: 'user123', token: 'abc'});
// encoded.value === "Bearer user123:abc"
// Decode: Header string -> AuthInfo
const decoded = AuthHeaderCodec.decode('Bearer user123:abc');
// decoded.value === {userId: 'user123', token: 'abc'}Chaining Codecs
const StringNumberCodec = new GGCodec({
encode: new GGTransform(IsString, IsNumber, (s) => parseInt(s, 10)),
decode: new GGTransform(IsNumber, IsString, (n) => n.toString())
});
const NumberBoolCodec = new GGCodec({
encode: new GGTransform(IsNumber, IsString, (n) => n > 0 ? "positive" : "zero"),
decode: new GGTransform(IsString, IsNumber, (s) => s === "positive" ? 1 : 0)
});
// Chain codecs: String <-> Number <-> String
const chained = StringNumberCodec.codecTo(NumberBoolCodec);
chained.encode("42"); // "positive"
chained.decode("positive"); // "1"Use Cases
API Header Transformation
const AuthHeaderCodec = new GGCodec({
encode: new GGTransform(AuthInfoSchema, IsString, (info) => `Bearer ${info.token}`),
decode: new GGTransform(IsString, AuthInfoSchema, parseAuthHeader)
});
// Server: decode incoming header
const authInfo = AuthHeaderCodec.decode(req.headers.authorization);
// Client: encode outgoing header
const header = AuthHeaderCodec.encode({userId: '123', token: 'abc'});Data Serialization
// IsDate validates YYYY-MM-DD string format
// Transform between date string and timestamp number
const DateTimestampCodec = new GGCodec({
encode: new GGTransform(IsDate, IsNumber, (dateStr) => new Date(dateStr).getTime()),
decode: new GGTransform(IsNumber, IsDate, (ts) => new Date(ts).toISOString().split('T')[0])
});
// Encode: "2024-01-15" -> 1705276800000
const timestamp = DateTimestampCodec.encode("2024-01-15");
// Decode: 1705276800000 -> "2024-01-15"
const dateStr = DateTimestampCodec.decode(1705276800000);OpenAPI Documentation
Since transforms preserve both input and output schemas, tools can generate accurate API documentation:
const transform = new GGTransform(InputSchema, OutputSchema, transformFn);
// Access schemas for documentation generation
transform.inputSchema // Request body schema
transform.outputSchema // Wire format schemaBest Practices
Validate Output Schemas
Always define accurate output schemas. Output validation catches bugs in your transform functions early:
// Good - output schema matches what transform returns
const Transform = new GGTransform(
IsString,
IsObject({id: IsNumber, name: IsString}),
(s) => ({id: parseInt(s), name: `User ${s}`})
);
// Bad - output schema doesn't match, will throw at runtime
const BadTransform = new GGTransform(
IsString,
IsNumber, // Says it returns number
(s) => ({id: s}) // Actually returns object - throws!
);Use Codecs for Bidirectional Data
When data flows both ways (client/server, serialize/deserialize), use GGCodec:
// Good - bidirectional transformation
const UserCodec = new GGCodec({
encode: new GGTransform(UserSchema, UserDTOSchema, toDTO),
decode: new GGTransform(UserDTOSchema, UserSchema, fromDTO)
});
// Avoid - separate unrelated transforms
const toDTO = new GGTransform(UserSchema, UserDTOSchema, ...);
const fromDTO = new GGTransform(UserDTOSchema, UserSchema, ...);Chain vs Compose
Use transformTo/codecTo for pipelines, not nested function calls:
// Good - composable chain
const pipeline = step1.transformTo(step2).transformTo(step3);
// Avoid - nested calls lose schema metadata
const result = step3.encode(step2.encode(step1.encode(input)));