{"id":17929217,"url":"https://github.com/skarab42/tson","last_synced_at":"2025-06-28T13:41:29.580Z","repository":{"id":43087868,"uuid":"469122033","full_name":"skarab42/tson","owner":"skarab42","description":"Type Safe Object Notation \u0026 Validation","archived":false,"fork":false,"pushed_at":"2023-10-08T10:13:35.000Z","size":445,"stargazers_count":10,"open_issues_count":6,"forks_count":1,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-05-24T18:19:33.495Z","etag":null,"topics":["javascript","json","schema","serialization","type-safe","type-safety","typescript","validation"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/skarab42.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-03-12T15:28:33.000Z","updated_at":"2024-02-28T04:36:13.000Z","dependencies_parsed_at":"2024-10-28T21:08:16.682Z","dependency_job_id":"49da7b18-573d-4a48-8656-e838d9d8cd00","html_url":"https://github.com/skarab42/tson","commit_stats":{"total_commits":63,"total_committers":2,"mean_commits":31.5,"dds":"0.015873015873015928","last_synced_commit":"681c4f24a4e574bf30674cb362eb2c2b90f560b5"},"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"purl":"pkg:github/skarab42/tson","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skarab42%2Ftson","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skarab42%2Ftson/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skarab42%2Ftson/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skarab42%2Ftson/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/skarab42","download_url":"https://codeload.github.com/skarab42/tson/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/skarab42%2Ftson/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262257089,"owners_count":23283186,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["javascript","json","schema","serialization","type-safe","type-safety","typescript","validation"],"created_at":"2024-10-28T21:08:13.051Z","updated_at":"2025-06-28T13:41:29.545Z","avatar_url":"https://github.com/skarab42.png","language":"TypeScript","funding_links":["https://github.com/sponsors/skarab42"],"categories":[],"sub_categories":[],"readme":"# tson\n\n**Type Safe Object Notation \u0026 Validation**\n\n[![Test and Lint](https://github.com/skarab42/tson/actions/workflows/CI.yaml/badge.svg)](https://github.com/skarab42/tson/actions/workflows/CI.yaml) [![codecov](https://codecov.io/gh/skarab42/tson/branch/main/graph/badge.svg?token=4PSFJBVAFB)](https://codecov.io/gh/skarab42/tson) ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/skarab42/tson?color=success\u0026style=flat) ![GitHub](https://img.shields.io/github/license/skarab42/tson?color=success) [![GitHub Sponsors](https://img.shields.io/github/sponsors/skarab42?color=ff69b4\u0026label=%E2%9D%A4%20sponsors%20)](https://github.com/sponsors/skarab42) [![Twitch Status](https://img.shields.io/twitch/status/skarab42?style=social)](https://www.twitch.tv/skarab42)\n\n📌 Work in Progress, not ready for production...\n\n## Features\n\n- 🧱 Functional\n- 🔷 Immutable\n- ✅ Well tested\n\n## Why?\n\nAfter a contribution to the [tRPC](https://github.com/trpc/trpc) project, I wanted to understand more deeply the use of generics and inference in TypeScript.\nI needed a challenge so I set myself the goal of coding my own schema validation library.\nThis library is heavily inspired by [Zod](https://github.com/colinhacks/zod) (_I try to provide the same API_) but in order to avoid cloning it, I challenged myself to not use any classes.\n\n# Install\n\n```bash\npnpm add @skarab/tson\n```\n\n_`yarn` and `npm` also works_\n\n## ES and CommonJS module\n\n```ts\nimport { t } from \"tson\";\n```\n\n```ts\nconst { t } = require(\"tson\");\n```\n\n# Examples\n\n```ts\nimport { t } from \"tson\";\n\nconst name = t.string();\n\nname.parse(\"nyan\"); // return \"nyan\"\nname.parse(42); // throw TypeCheckError\n```\n\n```ts\nimport { t } from \"tson\";\n\nconst user = t.object({\n  name: t.string(),\n  age: t.number(),\n  admin: t.boolean(),\n});\n\nuser.parse({ name: \"nyan\", age: 42, admin: true });\n\ntype User = t.infer\u003ctypeof user\u003e;\n// { name: string, age: number, admin: boolean }\n```\n\n# Strict mode\n\n## TypeScript\n\nIt is strongly recommended to activate the [strict](https://www.typescriptlang.org/tsconfig#strict) mode of TypeScript which will activate all checking behaviours that results in stronger guarantees of the program's correctness.\n\n## tson\n\nBy default `tson` parse objects in `STRICT` mode, this means that all undefined values in a scheme will be considered as an error. You can change this behaviour globally or locally, the procedure is documented [here](#objectschema-mode).\n\n# Table of contents\n\n- [tson](#tson)\n  - [Features](#features)\n  - [Why?](#why)\n- [Install](#install)\n  - [ES and CommonJS module](#es-and-commonjs-module)\n- [Examples](#examples)\n- [Strict mode](#strict-mode)\n  - [TypeScript](#typescript)\n  - [tson](#tson-1)\n- [Table of contents](#table-of-contents)\n- [API](#api)\n  - [First level types](#first-level-types)\n    - [Primitive types](#primitive-types)\n    - [Numbers types](#numbers-types)\n    - [Empty types](#empty-types)\n    - [Catch-all types](#catch-all-types)\n    - [Never type](#never-type)\n  - [literal(value)](#literalvalue)\n  - [array(type)](#arraytype)\n  - [tuple(...type)](#tupletype)\n  - [tuple(type[])](#tupletype-1)\n  - [tuple(type[] as const)](#tupletype-as-const)\n  - [object(schema)](#objectschema)\n  - [object(schema, mode)](#objectschema-mode)\n  - [object helpers](#object-helpers)\n    - [.strict()](#strict)\n    - [.strip()](#strip)\n    - [.passthrough()](#passthrough)\n  - [union(...type)](#uniontype)\n  - [union(type[])](#uniontype-1)\n  - [union(type[] as const)](#uniontype-as-const)\n  - [optional(type)](#optionaltype)\n  - [enum(...string)](#enumstring)\n    - [Access enum properties](#access-enum-properties)\n    - [Access enum values](#access-enum-values)\n    - [Test enum values](#test-enum-values)\n    - [Infer enum type](#infer-enum-type)\n  - [enum(string[])](#enumstring-1)\n  - [enum(string[] as const)](#enumstring-as-const)\n  - [enum(object)](#enumobject)\n  - [enum(object as const)](#enumobject-as-const)\n  - [enum(enum)](#enumenum)\n  - [nativeEnum(enum)](#nativeenumenum)\n  - [instanceof(type)](#instanceoftype)\n  - [date()](#date)\n  - [record(type)](#recordtype)\n  - [set(type)](#settype)\n  - [set(...type)](#settype-1)\n  - [set([type, ...type])](#settype-type)\n  - [map(keyType, valueType)](#mapkeytype-valuetype)\n  - [map(schema)](#mapschema)\n  - [promise(type)](#promisetype)\n  - [function()](#function)\n  - [function(args)](#functionargs)\n  - [function(args, returns)](#functionargs-returns)\n  - [function(args, returns, implement)](#functionargs-returns-implement)\n  - [preprocess(filter, type)](#preprocessfilter-type)\n  - [postprocess(filter, type)](#postprocessfilter-type)\n  - [postprocess(filter, inputType, outputType)](#postprocessfilter-inputtype-outputtype)\n- [Type helpers](#type-helpers)\n  - [safeParse(input)](#safeparseinput)\n  - [optional()](#optional)\n  - [preprocess()](#preprocess)\n  - [postprocess()](#postprocess)\n- [Contributing 💜](#contributing-)\n\n# API\n\n## First level types\n\n### Primitive types\n\n```ts\nt.string();\nt.number();\nt.bigint();\nt.boolean();\nt.symbol();\nt.date();\n```\n\n### Numbers types\n\n```ts\nt.nan();\nt.finite();\nt.infinity();\nt.integer(); // Alias: int()\nt.unsignedNumber(); // Alias: unumber()\nt.unsignedInteger(); // Alias: uinteger(), uint()\n```\n\n### Empty types\n\n```ts\nt.undefined();\nt.null();\nt.void();\n```\n\n### Catch-all types\n\n```ts\nt.any();\nt.unknown();\n```\n\n### Never type\n\n```ts\nt.never();\n```\n\n## literal(value)\n\n```ts\nconst life = t.literal(42);\nconst love = t.literal(true);\nconst name = t.literal(\"nyan\");\n\nlife.value; // type =\u003e 42\n```\n\n## array(type)\n\n```ts\nconst arr1 = t.array(t.string()); // string[]\nconst arr2 = t.array(t.boolean()); // boolean[]\n```\n\n## tuple(...type)\n\n```ts\nconst tpl = t.tuple(t.string(), t.number(), t.string()); // [string, number, string]\n```\n\n## tuple(type[])\n\n```ts\nconst tpl = t.tuple([t.string(), t.number(), t.string()]); // [string, number, string]\n```\n\n💔 The following code does not work, TypeScript can not infer array values properly. Use the `as const` workaround to do this.\n\n```ts\nconst types = [t.string(), t.number(), t.string()];\nconst tpl = t.tuple(types); // [string, number, string]\n```\n\n## tuple(type[] as const)\n\n```ts\nconst types = [t.string(), t.number(), t.string()] as const;\nconst tpl = t.tuple(types); // [string, number, string]\n```\n\n## object(schema)\n\n```ts\nconst user = t.object({\n  name: t.string(),\n  age: t.number(),\n  admin: t.boolean(),\n});\n\ntype User = t.infer\u003ctypeof user\u003e;\n// { name: string, age: number, admin: boolean }\n```\n\n## object(schema, mode)\n\nBy default `tson` parse objects in `STRICT` mode, but you can change the mode globally or locally.\n\nThere are three modes:\n\n- `STRICT`: Will raise an error if a key is not defined in the schema.\n- `STRIP`: Strips undefined keys from the result and does not raise an error.\n- `PASSTHROUGH`: Keeps undefined keys and does not raise an error.\n\nChange the default mode globally.\n\n```ts\nt.defaultSettings.objectTypeMode = t.ObjectTypeMode.STRIP;\n```\n\nChange the mode locally.\n\n```ts\nconst schema = { a: t.string(), b: t.string() };\nconst input = { a: \"a\", b: \"b\", c: \"c\" };\n\nconst user = t.object(schema, t.ObjectTypeMode.STRICT);\nuser.parse(input); // throws an TypeParseError\n\nconst user = t.object(schema, t.ObjectTypeMode.STRIP);\nuser.parse(input); // { a: string, b: string }\n\nconst user = t.object(schema, t.ObjectTypeMode.PASSTHROUGH);\nuser.parse(input); // { a: string, b: string, c: string }\n```\n\n## object helpers\n\n### .strict()\n\n```ts\nt.object(schema).strict();\n// same as\nt.object(schema, t.ObjectTypeMode.STRICT);\n```\n\n### .strip()\n\n```ts\nt.object(schema).strip();\n// same as\nt.object(schema, t.ObjectTypeMode.STRIP);\n```\n\n### .passthrough()\n\n```ts\nt.object(schema).passthrough();\n// same as\nt.object(schema, t.ObjectTypeMode.PASSTHROUGH);\n```\n\n## union(...type)\n\n```ts\nconst uni = t.union(t.string(), t.number()); // string | number\n```\n\n## union(type[])\n\n```ts\nconst tpl = t.union([t.string(), t.number(), t.string()]); // string | number\n```\n\n💔 The following code does not work, TypeScript can not infer array values properly. Use the `as const` workaround to do this.\n\n```ts\nconst types = [t.string(), t.number(), t.string()];\nconst tpl = t.union(types); // string | number\n```\n\n## union(type[] as const)\n\n```ts\nconst types = [t.string(), t.number(), t.string()] as const;\nconst tpl = t.union(types); // string | number\n```\n\n## optional(type)\n\n```ts\nconst user = t.object({\n  name: t.string(),\n  age: t.optional(t.number()),\n});\n// { name: string, age?: number }\n```\n\n## enum(...string)\n\n```ts\nconst myEnum = t.enum(\"UP\", \"DOWN\", \"LEFT\", \"RIGHT\");\n```\n\n### Access enum properties\n\n```ts\nmyEnum.enum.UP; // === \"UP\"\nmyEnum.enum.PLOP; // error: PLOP does not exists\nmyEnum.enum.DOWN = \"prout\"; // error: it is read-only\n\n(property) enum: {\n  readonly UP: \"UP\";\n  readonly DOWN: \"DOWN\";\n  readonly LEFT: \"LEFT\";\n  readonly RIGHT: \"RIGHT\";\n}\n```\n\n### Access enum values\n\n```ts\nmyEnum.options[1]; // === \"DOWN\"\n\n(property) options: [\"UP\", \"DOWN\", \"LEFT\", \"RIGHT\"]\n```\n\n### Test enum values\n\n```ts\nmyEnum.parse(myEnum.enum.LEFT); // =\u003e \"LEFT\"\nmyEnum.parse(\"LEFT\"); // =\u003e \"LEFT\"\nmyEnum.parse(\"2\"); // =\u003e \"LEFT\"\nmyEnum.parse(2); // =\u003e \"LEFT\"\nmyEnum.parse(\"PLOP\"); // error: expected '0|1|2|3|UP|DOWN|LEFT|RIGHT' got 'string'\n```\n\n### Infer enum type\n\n```ts\ntype MyEnum = t.infer\u003ctypeof myEnum\u003e; // =\u003e \"UP\" | \"DOWN\" | \"LEFT\" | \"RIGHT\"\n\nfunction move(direction: MyEnum) {\n  // direction === \"DOWN\"\n}\n\nmove(myEnum.enum.DOWN);\n```\n\n## enum(string[])\n\n```ts\nconst myEnum = t.enum([\"UP\", \"DOWN\", \"LEFT\", \"RIGHT\"]);\n```\n\n💔 The following code does not work, TypeScript can not infer array values properly. Use the `as const` workaround to do this.\n\n```ts\nconst values = [\"UP\", \"DOWN\", \"LEFT\", \"RIGHT\"];\nconst myEnum = t.enum(values);\n```\n\n## enum(string[] as const)\n\n```ts\nconst myEnum = t.enum([\"UP\", \"DOWN\", \"LEFT\", \"RIGHT\"] as const);\n```\n\n```ts\nconst values = [\"UP\", \"DOWN\", \"LEFT\", \"RIGHT\"] as const;\nconst myEnum = t.enum(values);\n```\n\n## enum(object)\n\n```ts\nconst myEnum = t.enum({ UP: \"UP\", DOWN: \"DOWN\", LEFT: 42, RIGHT: 43 });\n```\n\n💔 The following code does not work, TypeScript can not infer object properties properly. Use the `as const` workaround to do this.\n\n```ts\nconst values = { UP: \"UP\", DOWN: \"DOWN\", LEFT: 42, RIGHT: 43 };\nconst myEnum = t.enum(values);\n```\n\n## enum(object as const)\n\n```ts\nconst values = { UP: \"UP\", DOWN: \"DOWN\", LEFT: 42, RIGHT: 43 } as const;\nconst myEnum = t.enum(values);\n```\n\n## enum(enum)\n\n```ts\nenum MyEnum {\n  UP = \"UP\",\n  DOWN = \"DOWN\",\n  LEFT = 42,\n  RIGHT,\n}\n\nconst myEnum = t.enum(MyEnum);\n```\n\n## nativeEnum(enum)\n\nAlias: `enum(enum)`\n\n```ts\nenum MyEnum {\n  UP = \"UP\",\n  DOWN = \"DOWN\",\n  LEFT = 42,\n  RIGHT,\n}\n\nconst myEnum = t.nativeEnum(MyEnum);\n```\n\n## instanceof(type)\n\n```ts\nclass MyClass {}\n\nconst instance = new MyClass();\n\nt.instanceof(MyClass).parse(instance); // passes\nt.instanceof(MyClass).parse(\"nyan\"); // fail\n```\n\n## date()\n\n```ts\nt.date().parse(new Date()); // passes\nt.date().parse(\"2022-01-12T00:00:00.000Z\"); // passes\nt.date().parse(\"not a string date\"); // fail\n```\n\n## record(type)\n\n```ts\nt.record(t.string()); // { [x: string]: string }\nt.record(t.number()); // { [x: string]: number }\nt.record(t.date()); // { [x: string]:  Date }\n```\n\n## set(type)\n\nTesting a single type on the entire set\n\n```ts\nt.set(t.string()); // Set\u003cstring\u003e\n```\n\nTesting a union of types on the entire set\n\n```ts\nt.set(t.union(t.string(), t.boolean(), t.string())); // Set\u003cstring|boolean\u003e\n```\n\n## set(...type)\n\nSame as [tuple(...type)](#tupletype) but test if the input is an instance of Set.\n\n## set([type, ...type])\n\nTesting a tuple of types on the Set\n\n```ts\nt.set(t.string(), t.boolean(), t.string()); // Set\u003c[string, boolean, string]\u003e\nt.set([t.string(), t.boolean(), t.string()]); // Set\u003c[string, boolean, string]\u003e\n```\n\n## map(keyType, valueType)\n\n```ts\nt.map(t.string(), t.number()); // Map\u003cstring, number\u003e\nt.map(t.date(), t.string()); // Map\u003cDate, string\u003e\n```\n\n## map(schema)\n\nSame as [object(schema)](#objectschema) but test if the input is an instance of Map.\n\n```ts\nconst map = new Map();\n\nt.map({ name: t.string(), size: t.string() }).parse(map);\n```\n\n## promise(type)\n\n```ts\nconst promise = t.promise(t.number());\n\nawait promise.parse(Promise.resolve(42)); // resolve: 42\nawait promise.parse(Promise.resolve(\"42\")); // reject: expected 'number' got 'string'\nawait promise.parse(42); // reject: expected 'Promise' got 'number'\n```\n\n## function()\n\n```ts\nconst func = t.function();\n\ntype Func = t.infer\u003ctypeof func\u003e; // () =\u003e void\n```\n\n## function(args)\n\n```ts\nconst func = t.function([t.string(), t.number()]);\n\ntype Func = t.infer\u003ctypeof func\u003e; // (arg_0: string, arg_1: number) =\u003e void\n```\n\n## function(args, returns)\n\n```ts\nconst func = t.function([t.string()], t.boolean());\n\ntype Func = t.infer\u003ctypeof func\u003e; // (arg_0: string) =\u003e boolean\n```\n\n## function(args, returns, implement)\n\n```ts\nconst args = [t.string(), t.boolean()] as const;\n\nconst returns = t.union(t.string(), t.number());\n\nconst func = t.function(args, returns, (input, toInt) =\u003e {\n  // input type is string and toInt type is boolean\n  return toInt ? parseInt(input) : input.toUpperCase();\n});\n\ntype Func = t.infer\u003ctypeof func\u003e; // (arg_0: string, arg_1: boolean) =\u003e string | number\n```\n\n## preprocess(filter, type)\n\nIf you want to modify the input before it is parsed you can use the `preprocess` type as follows.\n\n```ts\nconst toString = t.preprocess((input) =\u003e String(input), t.string());\n\ntoString.parse(\"42\"); // =\u003e \"42\"\ntoString.parse(42); // =\u003e \"42\"\n```\n\n## postprocess(filter, type)\n\nIf you want to modify the output after it is parsed you can use the `postprocess` type as follows.\n\n```ts\nconst postprocess = t.postprocess((input) =\u003e input + 2, t.number());\n\npostprocess.parse(40); // =\u003e 42\npostprocess.parse(\"42\"); // throws: \"expected 'number' got 'string'\"\n```\n\n## postprocess(filter, inputType, outputType)\n\nIf you want to modify the output after it is parsed you can use the `postprocess` type as follows.\n\n```ts\nconst postprocess = t.postprocess(\n  (input) =\u003e String(input),\n  t.number(),\n  t.string(),\n);\n\npostprocess.parse(40); // =\u003e \"42\"\npostprocess.parse(\"42\"); // =\u003e throws: \"expected 'number' got 'string'\"\n```\n\n# Type helpers\n\n## safeParse(input)\n\nIf you want to avoid the parse method throws an error you can use the `.safeParse()` method instead.\n\n```ts\nt.bigint().safeParse(42n);\n// =\u003e { success: true, data: 42n }\n\nt.bigint().safeParse(42);\n// =\u003e {\n//   \"error\": [TypeParseError: expected 'bigint|undefined' got 'number'],\n//   \"success\": false,\n// }\n```\n\n## optional()\n\n```ts\nt.bigint().optional(); // =\u003e bigint | undefined\n\n// same as\nt.optional(t.bigint());\n```\n\n## preprocess()\n\n```ts\nt.string().preprocess((input) =\u003e String(input));\n\n// same as\nt.preprocess((input) =\u003e String(input), t.string());\n```\n\n## postprocess()\n\nAlias: `.transform()`\n\n```ts\nt.number().postprocess((input) =\u003e input + 2);\n\n// same as\nt.postprocess((input) =\u003e input + 2, t.number());\n```\n\n# Contributing 💜\n\nSee [CONTRIBUTING.md](https://github.com/skarab42/tson/blob/main/CONTRIBUTING.md)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskarab42%2Ftson","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fskarab42%2Ftson","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fskarab42%2Ftson/lists"}