| name | tsentials-json |
| description | Use when parsing or stringifying JSON safely without exceptions — safeJsonParse/safeJsonStringify return Result<T> instead of throwing, parseAndValidate combines parsing with a custom type guard, and Json/JsonObject/JsonArray/JsonPrimitive types plus isJson/isJsonObject guards enforce structural validity at compile and runtime. |
tsentials/json
Type-safe JSON parsing and validation that returns Result<T> — no exceptions, no try/catch, fits directly into the railway pipeline.
Installation
npm install tsentials
Import
import { safeJsonParse, safeJsonStringify, parseAndValidate } from 'tsentials/json';
import { isJson, isJsonObject, isJsonArray, isJsonPrimitive } from 'tsentials/json';
import type { Json, JsonObject, JsonArray, JsonPrimitive } from 'tsentials/json';
safeJsonParse
Parses a JSON string and returns Result<Json>. Never throws.
const result = safeJsonParse('{"name":"Alice","age":30}');
if (result.ok) {
console.log(result.value);
} else {
console.error(result.errors[0].code);
}
safeJsonStringify
Stringifies a Json value and returns Result<string>. Catches circular reference errors.
const result = safeJsonStringify({ id: 1, tags: ['a', 'b'] });
if (result.ok) {
console.log(result.value);
} else {
console.error(result.errors[0].code);
}
parseAndValidate
Parses JSON and validates with a custom type guard. Returns Result<T> — typed to your domain type.
import { isJsonObject } from 'tsentials/json';
interface User { name: string; age: number }
function isUser(value: unknown): value is User {
return isJsonObject(value) && typeof value.name === 'string' && typeof value.age === 'number';
}
const result = parseAndValidate<User>('{"name":"Alice","age":30}', isUser);
if (result.ok) {
console.log(result.value.name);
}
Type Guards
isJsonPrimitive('hello');
isJsonPrimitive(undefined);
isJsonArray([1, 'a', null]);
isJsonArray({});
isJsonObject({ key: 'value' });
isJsonObject(new Date());
isJsonObject([]);
isJson({ nested: [1, null] });
isJson({ fn: () => {} });
isJson({ key: undefined });
Important: isJsonObject only accepts plain objects (Object.prototype or null prototype). new Date(), new Map(), class instances all return false.
Error Codes
| Code | Cause |
|---|
Json.SyntaxError | JSON.parse threw — malformed input string |
Json.ValidationError | Parsed successfully but failed isJson or custom guard |
Json.StringifyFailed | JSON.stringify threw — typically a circular reference |
Pipeline Integration
safeJsonParse returns Result<Json> so it chains directly into any railway pipeline:
import { Result } from 'tsentials/result';
import { safeJsonParse, isJsonObject, parseAndValidate } from 'tsentials/json';
const processed = Result.then(
safeJsonParse(rawInput),
data => validatePayload(data),
);
const response = await fromAsync(fetchRawBody(req))
.andThen(body => safeJsonParse(body))
.andThen(data => parseAndValidate(JSON.stringify(data), isUser))
.map(user => enrichUser(user))
.match(
user => ({ status: 200, body: user }),
errs => ({ status: 400, body: errs[0]?.description }),
);
Json Types
type JsonPrimitive = string | number | boolean | null;
type JsonArray = readonly Json[];
interface JsonObject { readonly [key: string]: Json | undefined }
type Json = JsonPrimitive | JsonArray | JsonObject;
All types are readonly — immutability is enforced at compile time.
Best Practices
- Use
safeJsonParse instead of JSON.parse — eliminates uncaught SyntaxError exceptions
- Use
parseAndValidate to combine parsing and domain validation in one step
- Combine with
Err.fromException only if wrapping legacy code that throws — prefer safeJsonParse natively
isJsonObject is the right guard to use inside parseAndValidate type guards — it rejects class instances that typeof x === 'object' would accept
- Do not use
Json as a domain type — parse to a specific interface with parseAndValidate instead