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.
# npm
npm install decodable-js
# yarn
yarn add decodable-jsimport { 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
// }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' } } }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] }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' }] }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' }]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 levelconst 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.
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 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']| 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 |
| 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 |
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 objectErrors 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 found
Alex Shumihin — initial work and maintenance.
For feedback or issues, please open a GitHub issue or submit a pull request.