decodable-js
v2.0.5
Published
`decodable-js` is a TypeScript/JavaScript library inspired by Swift's Codable. It validates types in JSON data, strips unknown fields, and returns a clean object shaped exactly like your schema. Based on configuration, it can either throw on type mismatch
Readme
decodable-js
Overview
decodable-js is a TypeScript/JavaScript library inspired by Swift's Codable. It validates types in JSON data, strips unknown fields, and returns a clean object shaped exactly like your schema. Based on configuration, it can either throw on type mismatches or silently skip misaligned fields. It also supports optional fields and string↔number conversion.
The return type is fully inferred from the struct via the Decode<T> conditional type — no manual type annotations needed.
Installation
# npm
npm install decodable-js
# yarn
yarn add decodable-jsUsage
Basic — flat object
import { decodable, T } from 'decodable-js';
const struct = {
age: T.number,
address: T.string,
visible: T.boolean,
salary: T.number_$ // optional
}
const data = {
age: 12,
address: '123 Maple Street',
visible: true,
salary: 5000,
secret: 'will be stripped'
}
const result = decodable({ data, struct })
// {
// age: 12,
// address: '123 Maple Street',
// visible: true,
// salary: 5000
// }Nested objects
Struct mirrors the shape of your data at any depth — unknown fields are stripped at every level:
const struct = {
user: {
name: T.string,
address: {
city: T.string,
country: T.string,
}
}
}
const data = {
user: {
name: 'Emily Johnson',
password: 'hidden', // stripped
address: {
city: 'Austin',
country: 'USA',
zip: '78701', // stripped
}
}
}
const result = decodable({ data, struct })
// { user: { name: 'Emily Johnson', address: { city: 'Austin', country: 'USA' } } }Arrays of primitives
Wrap the element type in an array:
const struct = { tags: [T.string], ids: [T.number] }
const data = { tags: ['js', 'ts'], ids: [1, 2, 3] }
decodable({ data, struct })
// { tags: ['js', 'ts'], ids: [1, 2, 3] }Arrays of objects
const struct = {
users: [{ id: T.number, name: T.string }]
}
const data = {
users: [
{ id: 1, name: 'Emily Johnson', secret: 'stripped' },
{ id: 2, name: 'Michael Smith', secret: 'stripped' },
]
}
decodable({ data, struct })
// { users: [{ id: 1, name: 'Emily Johnson' }, { id: 2, name: 'Michael Smith' }] }Top-level array
Pass arrays as both data and struct:
const data = [{ id: 1, name: 'Emily Johnson' }, { id: 2, name: 'Michael Smith' }]
const struct = [{ id: T.number, name: T.string }]
decodable({ data, struct })
// [{ id: 1, name: 'Emily Johnson' }, { id: 2, name: 'Michael Smith' }]Deep nesting (object → array of objects → array of objects)
Nesting is recursive — any combination of objects and arrays works:
const struct = {
company: {
name: T.string,
departments: [{
title: T.string,
employees: [{
id: T.number,
tags: [{ label: T.string }]
}]
}]
}
}
decodable({ data, struct }) // strips extra fields at every levelenableConvert — string ↔ number conversion
const struct = { numbers: [T.string] }
const data = { numbers: [1, '2', '3'] } // 1 will be converted
decodable({ data, struct, enableConvert: true })
// { numbers: ['1', '2', '3'] }If a string cannot be converted to a number (e.g.
'abc'), an error is thrown. UsesilentMode: trueto skip such values instead.
silentMode — skip mismatches instead of throwing
const struct = { id: T.number, name: T.string }
const data = { id: 'wrong', name: 'Emily Johnson' }
decodable({ data, struct, silentMode: true })
// { name: 'Emily Johnson' } — 'id' skipped, no error thrownonDrop — observe skipped fields
onDrop is called whenever a field is silently skipped in silentMode. Receives the dot-path and the offending value:
const dropped: string[] = []
decodable({
data: { id: 'wrong', name: 'Emily Johnson' },
struct: { id: T.number, name: T.string },
silentMode: true,
onDrop: (path, value) => dropped.push(`${path}: ${value}`)
})
// dropped → ['id: wrong']API Reference
decodable(options)
| Option | Type | Default | Description |
|---|---|---|---|
| data | object \| array | — | JSON data to decode |
| struct | object \| array | — | Schema defining expected types |
| enableConvert | boolean | false | Convert between strings and numbers when possible |
| silentMode | boolean | false | Skip invalid fields instead of throwing |
| onDrop | (path: string, value: unknown) => void | — | Called for each silently skipped field |
Type markers (T)
| Marker | Type | Description |
|---|---|---|
| T.number | number | Any number |
| T.string | string | Any string |
| T.boolean | boolean | true or false |
| T.null | null | Exactly null |
| T.object | Record<string, any> | Any object — contents are passed through without validation |
| T.array | any[] | Any array — contents are passed through without validation |
Optional types
Append _$ to mark a field as optional (type | undefined). If the field is absent, it is omitted from the result. If present, its type must match.
| Marker | Equivalent |
|---|---|
| T.number_$ | number \| undefined |
| T.string_$ | string \| undefined |
| T.boolean_$ | boolean \| undefined |
| T.null_$ | null \| undefined |
| T.object_$ | Record<string, any> \| undefined |
| T.array_$ | any[] \| undefined |
You can also create an optional wrapper for any custom type with T.optional():
T.optional(T.number) // same as T.number_$
T.optional({ id: T.number, name: T.string }) // optional nested objectError messages
Errors include the full field path so the problem is easy to locate:
"user.address.city" is type "number" but expected "string" (got: 42)
"users[1].id" is type "string" but expected "number" (got: "abc")
"profile" key not foundAuthor
Alex Shumihin — initial work and maintenance.
For feedback or issues, please open a GitHub issue or submit a pull request.
