A context-aware, declarative serialization and validation framework for JavaScript.
serde-js lets you define one schema that can:
- validate user input (writes)
- safely serialize output (reads)
- enforce conditional logic via context
- support nested objects and arrays
- aggregate errors with precise paths
Inspired by Django REST Framework, Zod, and Joi — but designed to stay lightweight, explicit, and extensible.
- Installation
- Core Concepts
- Quick Start
- Fields
- Serializer
- Read vs Write Modes
- Context-Aware Rules
- Validation
- Nested Serializers
- ArrayField
- Error Handling
- Schema Introspection
- Advanced Patterns
- Design Philosophy
npm install @anclatechs/serde-jsconst {
Serializer,
CharField,
NumberField,
IntegerField,
BooleanField,
DateTimeField,
DateField,
EmailField,
UrlField,
ArrayField,
ObjectField,
} = require("@anclatechs/serde-js");A single Serializer definition can:
- validate input (e.g. signup, update)
- serialize output (API responses)
- behave differently based on context
No duplicate schemas. No conditionals scattered across controllers.
Each field:
- knows whether it is required
- can be optional or have defaults
- supports custom validators
- can be read-only or write-only
- can be conditionally included
Context is an arbitrary object you pass at runtime. Example:
{
mode: "input" | "output",
isSignup: true,
userRole: "admin"
}Fields and validators can react to it.
const UserSerializer = new Serializer({
name: new CharField(),
age: new IntegerField().optional(),
email: new EmailField(),
});
const result = UserSerializer.serialize({
name: "Ada",
age: 30,
email: "ada@example.com",
});
console.log(result.data); // serialized output
console.log(result.errors); // error mapping
console.log(result.isValid()) // boolean status for any available error
console.log(result.verboseErrorList()) // simplified, "verbosed" errors data retunred as an Array of object; accessible via .message.All fields inherit from Field.
Supported features:
new Field()
.optional()
.default(value)
.validate(fn)
.onlyIf((ctx) => boolean)
.readOnly()
.writeOnly();CharField, in addition to the validate function, also provide helper methods:
.enumOptions([])
.minLength(num)
.maxLength(num)NumberField and IntegerField also both support the .min() and .max() helper methods.
| Field | Description |
|---|---|
CharField |
String values |
NumberField |
Any number |
IntegerField |
Integer-only |
BooleanField |
Boolean |
DateTimeField |
JS Date (ISO) |
DateField |
Date-only |
EmailField |
Email string |
UrlField |
URL string |
ArrayField |
Arrays |
ObjectField |
Nested objects |
const UserSerializer = new Serializer(schema, options);{
many?: boolean // expect an array of objects
}Often:
- some fields should only be accepted (password)
- some should only be returned (id, timestamps)
const UserSerializer = new Serializer({
id: new IntegerField().readOnly(),
email: new EmailField(),
password: new CharField().writeOnly(),
});UserSerializer.serialize(req.body, { mode: "input" });UserSerializer.serialize(user, { mode: "output" });Conditionally include a field.
role: new CharField().onlyIf((ctx) => ctx.userRole === "admin");password: new CharField().onlyIf((ctx) => ctx.isSignup);If the condition fails, the field is:
- not required
- not validated
- not serialized
age: new IntegerField().validate((v) => v >= 18 || "Must be 18+");Validators return:
true→ passfalseorstring→ fail
const min = (n) => (v) => v >= n || `Must be ≥ ${n}`;
const minLength = (n) => (v) => v.length >= n || `Min length ${n}`;password: new CharField().validate(minLength(8));const AddressSerializer = new Serializer({
street: new CharField(),
city: new CharField(),
});
const UserSerializer = new Serializer({
name: new CharField(),
address: new ObjectField(AddressSerializer),
});Nested errors are merged automatically:
address.city: Field is required
tags: new ArrayField(new CharField());posts: new ArrayField(PostSerializer);Each item is validated independently with full error paths:
posts[2].title: Field is required
Errors are aggregated on the serializer:
serializer.errors;Example:
{
"email": "Invalid EmailField",
"address.city": "Field is required",
"tags[1]": "Invalid CharField"
}No exceptions. Full visibility.
UserSerializer.describe();Returns:
{
email: {
type: "EmailField",
required: true
},
tags: {
type: "ArrayField",
child: "CharField"
}
}Useful for:
- documentation
- form generation
- schema inspection
password: new CharField(),
confirmPassword: new CharField().validate((v, ctx) =>
v === ctx.password || "Passwords do not match"
)salary: new NumberField().onlyIf((ctx) => ctx.userRole === "admin");new CharField().optional().onlyIf((ctx) => ctx.isUpdate);- Explicit over magical
- One schema, many flows
- Context drives behavior
- Errors should be precise
- Composition over inheritance
serde-js is intentionally unopinionated about:
- HTTP frameworks
- Databases
- ORMs
It fits cleanly into Express, Fastify, NestJS, or serverless setups.
This library is intentionally small but powerful; supporting optional fields, default values, custom validators, aggregated error reporting, and context-aware serialization. Fields are required by default, your may define defaults or validation rules, and can dynamically include or validate data based on your preferred runtime context. Validation errors are collected and returned in a structured, predictable format suitable for APIs and batch processing.
If you understand how context flows, you can model:
- signup
- updates
- admin overrides
- read/write separation
…without ever duplicating schemas.
📜 License
MIT — use it, hack it, ship it.
Happy building 🚀