Skip to content

pioner92/decodable-js

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

58 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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-js

Usage

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 level

enableConvert — 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. Use silentMode: true to 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 thrown

onDrop — 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 object

Error 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 found

Author

Alex Shumihin — initial work and maintenance.

For feedback or issues, please open a GitHub issue or submit a pull request.

Releases

No releases published

Packages

 
 
 

Contributors