Skip to content

About

Decode and encode request/response bodies by content type: json, jsonx, ndjson, url-encoded and yaml.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Repository files navigation

chubbyts-decode-encode

CI Coverage Status Mutation testing badge npm-version

bugs code_smells coverage duplicated_lines_density ncloc sqale_rating alert_status reliability_rating security_rating sqale_index vulnerabilities

Description

Decode and encode request/response bodies by content type: json, jsonx, ndjson, url-encoded and yaml.

Strings are decoded into plain data (objects, arrays, strings, numbers, booleans and null) and encoded back. Each format can be registered under additional content types, for example application/problem+json or application/yaml.

Requirements

Installation

Through NPM as @chubbyts/chubbyts-decode-encode.

npm i @chubbyts/chubbyts-decode-encode@^2.8.0

Usage

Data

Every type decoder returns and every type encoder accepts Data, the json data model:

type Data = { [key: string]: Data } | Array<Data> | string | number | boolean | null;

The matching type guards isNull, isBoolean, isNumber, isString, isArray and isObject (plain objects only) can be imported from @chubbyts/chubbyts-decode-encode/data.

import { isObject } from '@chubbyts/chubbyts-decode-encode/data';

isObject({ key: 'value' }); // true
isObject(new Date()); // false

Decoder

createDecoder

import { createDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/decoder';
import { createJsonTypeDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/json-type-decoder';

const decoder = createDecoder([createJsonTypeDecoder()]);
const data = decoder.decode('{"key":"value"}', 'application/json');
// data: { key: 'value' }
const contentTypes = decoder.contentTypes;
// contentTypes: ['application/json']

The Decoder type accepts an optional third argument context (Record<string, unknown>). The shipped decoder ignores it, it exists for custom Decoder implementations, for example one that decodes depending on the current user.

createJsonTypeDecoder

import { createJsonTypeDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/json-type-decoder';

const jsonTypeDecoder = createJsonTypeDecoder();
const data = jsonTypeDecoder.decode('{"key":"value"}');
// data: { key: 'value' }
const contentType = jsonTypeDecoder.contentType;
// contentType: 'application/json'

// any other json based content type, like `application/problem+json` (RFC 9457)
const problemJsonTypeDecoder = createJsonTypeDecoder('application/problem+json');

createJsonxTypeDecoder

import { createJsonxTypeDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/jsonx-type-decoder';

const jsonxTypeDecoder = createJsonxTypeDecoder();
const data = jsonxTypeDecoder.decode(`
<?xml version="1.0" encoding="UTF-8"?>
<json:object>
  <json:string name="key">value</json:string>
</json:object>
`);
// data: { key: 'value' }
const contentType = jsonxTypeDecoder.contentType;
// contentType: 'application/jsonx+xml'

createNdjsonTypeDecoder

NDJSON / JSON Lines: one json document per line, decoded into an array.

import { createNdjsonTypeDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/ndjson-type-decoder';

const ndjsonTypeDecoder = createNdjsonTypeDecoder();
const data = ndjsonTypeDecoder.decode('{"key":"value1"}\n{"key":"value2"}\n');
// data: [{ key: 'value1' }, { key: 'value2' }]
const contentType = ndjsonTypeDecoder.contentType;
// contentType: 'application/jsonl'

// any other ndjson / json lines content type, like `application/x-ndjson`
const xNdjsonTypeDecoder = createNdjsonTypeDecoder('application/x-ndjson');

// silently ignore empty lines
const lenientNdjsonTypeDecoder = createNdjsonTypeDecoder('application/jsonl', true);

\n and \r\n line endings are accepted, the line terminator after the last line is optional. Empty lines (including lines with only json whitespace: space, tab, \r) are not valid json and throw a DecodeError, unless ignoreEmptyLines (second argument) is true. The DecodeError message starts with the line number, for example Line 3: ....

There is no registered media type for ndjson / json lines, so the service factories register all common ones: application/jsonl, application/x-jsonl, text/jsonl, application/ndjson and application/x-ndjson.

createUrlEncodedTypeDecoder

import { createUrlEncodedTypeDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/url-encoded-type-decoder';

const urlEncodedTypeDecoder = createUrlEncodedTypeDecoder();
const data = urlEncodedTypeDecoder.decode('key=value');
// data: { key: 'value' }
const contentType = urlEncodedTypeDecoder.contentType;
// contentType: 'application/x-www-form-urlencoded'

Values are converted to their json type: true and false become booleans, null becomes null and a value becomes a number only if the number converts back to the exact same string, so 01 or 1.0 stay strings.

const data = urlEncodedTypeDecoder.decode('a=1&b=true&c=null&d=01&e[]=x');
// data: { a: 1, b: true, c: null, d: '01', e: ['x'] }

createYamlTypeDecoder

import { createYamlTypeDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/yaml-type-decoder';

const yamlTypeDecoder = createYamlTypeDecoder();
const data = yamlTypeDecoder.decode('key: value');
// data: { key: 'value' }
const contentType = yamlTypeDecoder.contentType;
// contentType: 'application/x-yaml'

// the registered media type `application/yaml` (RFC 9512)
const officialYamlTypeDecoder = createYamlTypeDecoder('application/yaml');

Encoder

createEncoder

import { createEncoder } from '@chubbyts/chubbyts-decode-encode/encoder/encoder';
import { createJsonTypeEncoder } from '@chubbyts/chubbyts-decode-encode/encoder/json-type-encoder';

const encoder = createEncoder([createJsonTypeEncoder()]);
const encodedData = encoder.encode({ key: 'value' }, 'application/json');
// encodedData: '{"key":"value"}'
const contentTypes = encoder.contentTypes;
// contentTypes: ['application/json']

The Encoder type accepts an optional third argument context (Record<string, unknown>). The shipped encoder ignores it, it exists for custom Encoder implementations, for example one that encodes depending on the current user.

createJsonTypeEncoder

import { createJsonTypeEncoder } from '@chubbyts/chubbyts-decode-encode/encoder/json-type-encoder';

const jsonTypeEncoder = createJsonTypeEncoder();
const encodedData = jsonTypeEncoder.encode({ key: 'value' });
// encodedData: '{"key":"value"}'
const contentType = jsonTypeEncoder.contentType;
// contentType: 'application/json'

// pretty printed
const prettyJsonTypeEncoder = createJsonTypeEncoder(true);

// any other json based content type, like `application/problem+json` (RFC 9457)
const problemJsonTypeEncoder = createJsonTypeEncoder(false, 'application/problem+json');

createJsonxTypeEncoder

import { createJsonxTypeEncoder } from '@chubbyts/chubbyts-decode-encode/encoder/jsonx-type-encoder';

const jsonxTypeEncoder = createJsonxTypeEncoder(true); // pretty printed, defaults to false
const encodedData = jsonxTypeEncoder.encode({ key: 'value' });
// encodedData:
// <?xml version="1.0" encoding="UTF-8"?>
// <json:object xsi:schemaLocation="http://www.datapower.com/schemas/json jsonx.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:json="http://www.ibm.com/xmlns/prod/2009/jsonx">
//   <json:string name="key">value</json:string>
// </json:object>
const contentType = jsonxTypeEncoder.contentType;
// contentType: 'application/jsonx+xml'

createNdjsonTypeEncoder

import { createNdjsonTypeEncoder } from '@chubbyts/chubbyts-decode-encode/encoder/ndjson-type-encoder';

const ndjsonTypeEncoder = createNdjsonTypeEncoder();
const encodedData = ndjsonTypeEncoder.encode([{ key: 'value1' }, { key: 'value2' }]);
// encodedData: '{"key":"value1"}\n{"key":"value2"}\n'
const contentType = ndjsonTypeEncoder.contentType;
// contentType: 'application/jsonl'

// any other ndjson / json lines content type, like `application/x-ndjson`
const xNdjsonTypeEncoder = createNdjsonTypeEncoder('application/x-ndjson');

Every line, including the last one, is terminated by \n. Only arrays can be encoded, any other value throws an EncodeError.

createUrlEncodedTypeEncoder

import { createUrlEncodedTypeEncoder } from '@chubbyts/chubbyts-decode-encode/encoder/url-encoded-type-encoder';

const urlEncodedTypeEncoder = createUrlEncodedTypeEncoder();
const encodedData = urlEncodedTypeEncoder.encode({ a: 1, b: true, c: null, d: [1, 2] });
// encodedData: 'a=1&b=true&c=null&d[0]=1&d[1]=2'
const contentType = urlEncodedTypeEncoder.contentType;
// contentType: 'application/x-www-form-urlencoded'

createYamlTypeEncoder

import { createYamlTypeEncoder } from '@chubbyts/chubbyts-decode-encode/encoder/yaml-type-encoder';

const yamlTypeEncoder = createYamlTypeEncoder();
const encodedData = yamlTypeEncoder.encode({ key: 'value' });
// encodedData: 'key: value\n'
const contentType = yamlTypeEncoder.contentType;
// contentType: 'application/x-yaml'

// the registered media type `application/yaml` (RFC 9512)
const officialYamlTypeEncoder = createYamlTypeEncoder('application/yaml');

Custom type decoders / encoders

A type decoder / encoder is a plain object with a contentType and a decode / encode function. This adds text/plain, where the body is a single string:

import type { Data } from '@chubbyts/chubbyts-decode-encode/data';
import { isString } from '@chubbyts/chubbyts-decode-encode/data';
import type { TypeDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/decoder';
import { createDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/decoder';
import type { TypeEncoder } from '@chubbyts/chubbyts-decode-encode/encoder/encoder';
import { EncodeError, createEncoder } from '@chubbyts/chubbyts-decode-encode/encoder/encoder';

const createTextTypeDecoder = (): TypeDecoder => ({
  decode: (encodedData: string): Data => encodedData,
  contentType: 'text/plain',
});

const createTextTypeEncoder = (): TypeEncoder => ({
  encode: (data: Data): string => {
    if (!isString(data)) {
      throw new EncodeError(`Unsupported value: ${JSON.stringify(data)}, expected a string`);
    }

    return data;
  },
  contentType: 'text/plain',
});

const decoder = createDecoder([createTextTypeDecoder()]);
const encoder = createEncoder([createTextTypeEncoder()]);

Errors

  • decode throws a DecodeError (@chubbyts/chubbyts-decode-encode/decoder/decoder) if the encoded data is invalid, for example a json syntax error. In a http context this usually maps to 400 Bad Request.
  • encode of the jsonx and ndjson type encoders throws an EncodeError (@chubbyts/chubbyts-decode-encode/encoder/encoder) if a value cannot be represented, for ndjson anything but an array.
  • decode / encode of the decoder / encoder throw an Error if the content type is not supported, the message lists the supported content types. In a http context this usually maps to 415 Unsupported Media Type (decoder) or 406 Not Acceptable (encoder).
import { DecodeError } from '@chubbyts/chubbyts-decode-encode/decoder/decoder';

try {
  decoder.decode('{', 'application/json');
} catch (e) {
  if (e instanceof DecodeError) {
    // invalid request body
  }
}

Service factories (chubbyts-dic-config)

The package ships service factories (abstract factories built on chubbyts-dic-config-factory) for a chubbyts-dic-config (or any chubbyts-dic-types compatible) container within @chubbyts/chubbyts-decode-encode/service-factory:

  • typeDecodersServiceFactory: all shipped type decoders (json, jsonx, ndjson as application/jsonl, application/x-jsonl, text/jsonl, application/ndjson and application/x-ndjson, urlencoded, yaml as application/x-yaml and application/yaml)
  • typeEncodersServiceFactory: all shipped type encoders (json, jsonx, ndjson as application/jsonl, application/x-jsonl, text/jsonl, application/ndjson and application/x-ndjson, urlencoded, yaml as application/x-yaml and application/yaml), config.debug enables pretty printing for json / jsonx
  • decoderServiceFactory: the decoder, built from the typeDecoders service if registered, the shipped type decoders otherwise
  • encoderServiceFactory: the encoder, built from the typeEncoders service if registered, the shipped type encoders otherwise
import type { ConfigFactory } from '@chubbyts/chubbyts-dic-config/dic-config';
import { createContainerByConfigFactory } from '@chubbyts/chubbyts-dic-config/dic-config';
import type { Decoder } from '@chubbyts/chubbyts-decode-encode/decoder/decoder';
import type { Encoder } from '@chubbyts/chubbyts-decode-encode/encoder/encoder';
import { decoderServiceFactory, encoderServiceFactory } from '@chubbyts/chubbyts-decode-encode/service-factory';

const container = createContainerByConfigFactory({
  debug: false,
  dependencies: {
    factories: new Map<string, ConfigFactory>([
      ['decoder', decoderServiceFactory()],
      ['encoder', encoderServiceFactory()],
    ]),
  },
})();

const decoder = container.get<Decoder>('decoder');
const encoder = container.get<Encoder>('encoder');

To replace the type decoders / encoders, register a typeDecoders / typeEncoders service (createTextTypeDecoder from Custom type decoders / encoders):

import type { TypeDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/decoder';
import { createJsonTypeDecoder } from '@chubbyts/chubbyts-decode-encode/decoder/json-type-decoder';

const container = createContainerByConfigFactory({
  dependencies: {
    factories: new Map<string, ConfigFactory>([
      ['decoder', decoderServiceFactory()],
      ['typeDecoders', (): Array<TypeDecoder> => [createJsonTypeDecoder(), createTextTypeDecoder()]],
    ]),
  },
})();

With names

To use different decoders / encoders in different parts of an application, the same factories can be registered multiple times with a name, which gets appended to each service id (decoderapi, typeDecodersapi, ...).

const container = createContainerByConfigFactory({
  dependencies: {
    factories: new Map<string, ConfigFactory>([
      ['decoder', decoderServiceFactory()],
      ['decoderapi', decoderServiceFactory('api')],
      ['typeDecodersapi', (): Array<TypeDecoder> => [createJsonTypeDecoder()]],
    ]),
  },
})();

const decoder = container.get<Decoder>('decoder'); // all shipped type decoders
const apiDecoder = container.get<Decoder>('decoderapi'); // json only

Copyright

2026 Dominik Zogg

About

Decode and encode request/response bodies by content type: json, jsonx, ndjson, url-encoded and yaml.

Topics

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages