@anclatechs/serde-js
v1.0.7
Published
Composable, schema-driven serialization and validation for JavaScript.
Maintainers
Readme
@anclatechs/serde-js
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.
Table of Contents
- 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
Installation
npm install @anclatechs/serde-jsconst {
Serializer,
CharField,
NumberField,
IntegerField,
BooleanField,
DateTimeField,
DateField,
EmailField,
UrlField,
ArrayField,
ObjectField,
JsonField,
} = require("@anclatechs/serde-js");Core Concepts
1. One schema — many use cases
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.
2. Fields are intelligent
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
3. Context drives behavior
Context is an arbitrary object you pass at runtime. Example:
{
mode: "input" | "output",
isSignup: true,
userRole: "admin"
}Fields and validators can react to it.
Quick Start
const UserSerializer = new Serializer({
name: new CharField(),
age: new IntegerField().optional(),
email: new EmailField(),
});
const result = UserSerializer.serialize({
name: "Ada",
age: 30,
email: "[email protected]",
});
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.Fields
Base Field
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.
Available Field Types
| 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 structured objects |
| JsonField | dynamic JSON object |
Serializer
Creating a Serializer
const UserSerializer = new Serializer(schema, options);Options
{
many?: boolean // expect an array of objects
}Read vs Write Modes
Why this matters
Often:
- some fields should only be accepted (password)
- some should only be returned (id, timestamps)
Example
const UserSerializer = new Serializer({
id: new IntegerField().readOnly(),
email: new EmailField(),
password: new CharField().writeOnly(),
});Input (write)
UserSerializer.serialize(req.body, { mode: "input" });Output (read)
UserSerializer.serialize(user, { mode: "output" });Context-Aware Rules
onlyIf(fn)
Conditionally include a field.
role: new CharField().onlyIf((ctx) => ctx.userRole === "admin");Signup-only fields
password: new CharField().onlyIf((ctx) => ctx.isSignup);If the condition fails, the field is:
- not required
- not validated
- not serialized
Default Values
The Field.default() method allows you to provide a value when the input does not specify one.
It can be either:
1. A constant value
const UserSerializer = new Serializer({
name: new CharField().default("Ada"),
});2. A function for dynamic computation
You can compute the default value based on other fields in the serializer or context:
const UserSerializer = new Serializer({
name: new CharField(),
age: new IntegerField(),
ageInTwoYears: new IntegerField().default((_, root, ctx) => root.age * 2).readOnly(),
});Parameters of the default function
| Parameter | Description |
| --------- | ---------------------------------------------------------------------------------------------------------------------- |
| value | The input value for this field. If the input is missing, it will be undefined. May be ignored and represented as _. |
| root | The partially built serialized object. Lets you reference other fields in the same serializer. |
| ctx | Optional context object passed to serialize(input, context). Useful for user info, request metadata, etc. |
⚠️ Order matters: The field you reference in root (e.g., age) must appear before the dependent field (ageInTwoYears) in the schema.
Validation
Custom Validators
age: new IntegerField().validate((v) => v >= 18 || "Must be 18+");Validators return:
true→ passfalseorstring→ fail
Reusable Validator Helpers
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));Nested Serializers
ObjectField
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 requiredArrayField
Array of scalars
tags: new ArrayField(new CharField());Array of objects
posts: new ArrayField(PostSerializer);Each item is validated independently with full error paths:
posts[2].title: Field is requiredError Handling
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.
Schema Introspection
describe()
UserSerializer.describe();Returns:
{
email: {
type: "EmailField",
required: true
},
tags: {
type: "ArrayField",
child: "CharField"
}
}Useful for:
- documentation
- form generation
- schema inspection
Advanced Patterns
Cross-field validation
password: new CharField(),
confirmPassword: new CharField().validate((v, ctx) =>
v === ctx.password || "Passwords do not match"
)Role-based schemas
salary: new NumberField().onlyIf((ctx) => ctx.userRole === "admin");Partial updates
new CharField().optional().onlyIf((ctx) => ctx.isUpdate);Design Philosophy
- 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.
Final Note
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 🚀
