# fast-json-stringify [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](http://standardjs.com/) ![Ci Workflow](https://github.com/fastify/fast-json-stringify/workflows/CI%20workflow/badge.svg) [![NPM downloads](https://img.shields.io/npm/dm/fast-json-stringify.svg?style=flat)](https://www.npmjs.com/package/fast-json-stringify) __fast-json-stringify__ is significantly faster than `JSON.stringify()` for small payloads. Its performance advantage shrinks as your payload grows. It pairs well with [__flatstr__](https://www.npmjs.com/package/flatstr), which triggers a V8 optimization that improves performance when eventually converting the string to a `Buffer`. ##### Benchmarks - Machine: `EX41S-SSD, Intel Core i7, 4Ghz, 64GB RAM, 4C/8T, SSD`. - Node.js `v10.15.2` ``` FJS creation x 8,951 ops/sec ±0.51% (92 runs sampled) JSON.stringify array x 5,146 ops/sec ±0.32% (97 runs sampled) fast-json-stringify array x 8,402 ops/sec ±0.62% (95 runs sampled) fast-json-stringify-uglified array x 8,474 ops/sec ±0.49% (93 runs sampled) JSON.stringify long string x 13,061 ops/sec ±0.25% (98 runs sampled) fast-json-stringify long string x 13,059 ops/sec ±0.21% (98 runs sampled) fast-json-stringify-uglified long string x 13,099 ops/sec ±0.14% (98 runs sampled) JSON.stringify short string x 6,295,988 ops/sec ±0.28% (98 runs sampled) fast-json-stringify short string x 43,335,575 ops/sec ±1.24% (86 runs sampled) fast-json-stringify-uglified short string x 40,042,871 ops/sec ±1.38% (93 runs sampled) JSON.stringify obj x 2,557,026 ops/sec ±0.20% (97 runs sampled) fast-json-stringify obj x 9,001,890 ops/sec ±0.48% (90 runs sampled) fast-json-stringify-uglified obj x 9,073,607 ops/sec ±0.41% (94 runs sampled) ``` #### Table of contents: - `Example` - `API` - `fastJsonStringify` - `Specific use cases` - `Required` - `Missing fields` - `Pattern Properties` - `Additional Properties` - `AnyOf` - `Reuse - $ref` - `Long integers` - `Uglify` - `Nullable` - `Caveat` - `Acknowledgements` - `License` ## Example ```js const fastJson = require('fast-json-stringify') const stringify = fastJson({ title: 'Example Schema', type: 'object', properties: { firstName: { type: 'string' }, lastName: { type: 'string' }, age: { description: 'Age in years', type: 'integer' }, reg: { type: 'string' } } }) console.log(stringify({ firstName: 'Matteo', lastName: 'Collina', age: 32, reg: /"([^"]|\\")*"/ })) ``` ## API ### fastJsonStringify(schema) Build a `stringify()` function based on [jsonschema](http://json-schema.org/). Supported types: * `'string'` * `'integer'` * `'number'` * `'array'` * `'object'` * `'boolean'` * `'null'` And nested ones, too. #### Specific use cases | Instance | Serialized as | | -------- | ---------------------------- | | `Date` | `string` via `toISOString()` | | `RegExp` | `string` | | `BigInt` | `integer` via `toString` | [JSON Schema built-in formats](https://json-schema.org/understanding-json-schema/reference/string.html#built-in-formats) for dates are supported and will be serialized as: | Format | Serialized format example | | ----------- | -------------------------- | | `date-time` | `2020-04-03T09:11:08.615Z` | | `date` | `2020-04-03` | | `time` | `09:11:08` | Example with a MomentJS object: ```javascript const moment = require('moment') const stringify = fastJson({ title: 'Example Schema with string date-time field', type: 'string', format: 'date-time' } console.log(stringify(moment())) // '"YYYY-MM-DDTHH:mm:ss.sssZ"' ``` #### Required You can set specific fields of an object as required in your schema by adding the field name inside the `required` array in your schema. Example: ```javascript const schema = { title: 'Example Schema with required field', type: 'object', properties: { nickname: { type: 'string' }, mail: { type: 'string' } }, required: ['mail'] } ``` If the object to stringify is missing the required field(s), `fast-json-stringify` will throw an error. #### Missing fields If a field *is present* in the schema (and is not required) but it *is not present* in the object to stringify, `fast-json-stringify` will not write it in the final string. Example: ```javascript const stringify = fastJson({ title: 'Example Schema', type: 'object', properties: { nickname: { type: 'string' }, mail: { type: 'string' } } }) const obj = { mail: 'mail@example.com' } console.log(stringify(obj)) // '{"mail":"mail@example.com"}' ``` #### Defaults `fast-json-stringify` supports `default` jsonschema key in order to serialize a value if it is `undefined` or not present. Example: ```javascript const stringify = fastJson({ title: 'Example Schema', type: 'object', properties: { nickname: { type: 'string', default: 'the default string' } } }) console.log(stringify({})) // '{"nickname":"the default string"}' console.log(stringify({nickname: 'my-nickname'})) // '{"nickname":"my-nickname"}' ``` #### Pattern properties `fast-json-stringify` supports pattern properties as defined by JSON schema. *patternProperties* must be an object, where the key is a valid regex and the value is an object, declared in this way: `{ type: 'type' }`. *patternProperties* will work only for the properties that are not explicitly listed in the properties object. Example: ```javascript const stringify = fastJson({ title: 'Example Schema', type: 'object', properties: { nickname: { type: 'string' } }, patternProperties: { 'num': { type: 'number' }, '.*foo$': { type: 'string' } } }) const obj = { nickname: 'nick', matchfoo: 42, otherfoo: 'str' matchnum: 3 } console.log(stringify(obj)) // '{"matchfoo":"42","otherfoo":"str","matchnum":3,"nickname":"nick"}' ``` #### Additional properties `fast-json-stringify` supports additional properties as defined by JSON schema. *additionalProperties* must be an object or a boolean, declared in this way: `{ type: 'type' }`. *additionalProperties* will work only for the properties that are not explicitly listed in the *properties* and *patternProperties* objects. If *additionalProperties* is not present or is set to `false`, every property that is not explicitly listed in the *properties* and *patternProperties* objects,will be ignored, as described in Missing fields. Missing fields are ignored to avoid having to rewrite objects before serializing. However, other schema rules would throw in similar situations. If *additionalProperties* is set to `true`, it will be used by `JSON.stringify` to stringify the additional properties. If you want to achieve maximum performance, we strongly encourage you to use a fixed schema where possible. The additional properties will always be serialzied at the end of the object. Example: ```javascript const stringify = fastJson({ title: 'Example Schema', type: 'object', properties: { nickname: { type: 'string' } }, patternProperties: { 'num': { type: 'number' }, '.*foo$': { type: 'string' } }, additionalProperties: { type: 'string' } }) const obj = { nickname: 'nick', matchfoo: 42, otherfoo: 'str' matchnum: 3, nomatchstr: 'valar morghulis', nomatchint: 313 } console.log(stringify(obj)) // '{"nickname":"nick","matchfoo":"42","otherfoo":"str","matchnum":3,"nomatchstr":"valar morghulis",nomatchint:"313"}' ``` #### AnyOf `fast-json-stringify` supports the anyOf keyword as defined by JSON schema. *anyOf* must be an array of valid JSON schemas. The different schemas will be tested in the specified order. The more schemas `stringify` has to try before finding a match, the slower it will be. *anyOf* uses [ajv](https://www.npmjs.com/package/ajv) as a JSON schema validator to find the schema that matches the data. This has an impact on performance—only use it as a last resort. Example: ```javascript const stringify = fastJson({ title: 'Example Schema', type: 'object', properties: { 'undecidedType': { 'anyOf': [{ type: 'string' }, { type: 'boolean' }] } } } ``` #### If/then/else `fast-json-stringify` supports `if/then/else` jsonschema feature. See [ajv documentation](https://ajv.js.org/keywords.html#ifthenelse). Example: ```javascript const stringify = fastJson({ 'type': 'object', 'properties': { }, 'if': { 'properties': { 'kind': { 'type': 'string', 'enum': ['foobar'] } } }, 'then': { 'properties': { 'kind': { 'type': 'string', 'enum': ['foobar'] }, 'foo': { 'type': 'string' }, 'bar': { 'type': 'number' } } }, 'else': { 'properties': { 'kind': { 'type': 'string', 'enum': ['greeting'] }, 'hi': { 'type': 'string' }, 'hello': { 'type': 'number' } } } }) console.log(stringify({ kind: 'greeting', foo: 'FOO', bar: 42, hi: 'HI', hello: 45 })) // {"kind":"greeting","hi":"HI","hello":45} console.log(stringify({ kind: 'foobar', foo: 'FOO', bar: 42, hi: 'HI', hello: 45 })) // {"kind":"foobar","foo":"FOO","bar":42} ``` **NB:** don't declare the properties twice or you'll print them twice! #### Reuse - $ref If you want to reuse a definition of a value, you can use the property `$ref`. The value of `$ref` must be a string in [JSON Pointer](https://tools.ietf.org/html/rfc6901) format. Example: ```javascript const schema = { title: 'Example Schema', definitions: { num: { type: 'object', properties: { int: { type: 'integer' } } }, str: { type: 'string' } }, type: 'object', properties: { nickname: { $ref: '#/definitions/str' } }, patternProperties: { 'num': { $ref: '#/definitions/num' } }, additionalProperties: { $ref: '#/definitions/def' } } const stringify = fastJson(schema) ``` If you need to use an external definition, you can pass it as an option to `fast-json-stringify`. Example: ```javascript const schema = { title: 'Example Schema', type: 'object', properties: { nickname: { $ref: 'strings#/definitions/str' } }, patternProperties: { 'num': { $ref: 'numbers#/definitions/num' } }, additionalProperties: { $ref: 'strings#/definitions/def' } } const externalSchema = { numbers: { definitions: { num: { type: 'object', properties: { int: { type: 'integer' } } } } }, strings: require('./string-def.json') } const stringify = fastJson(schema, { schema: externalSchema }) ``` External definitions can also reference each other. Example: ```javascript const schema = { title: 'Example Schema', type: 'object', properties: { foo: { $ref: 'strings#/definitions/foo' } } } const externalSchema = { strings: { definitions: { foo: { $ref: 'things#/definitions/foo' } } }, things: { definitions: { foo: { type: 'string' } } } } const stringify = fastJson(schema, { schema: externalSchema }) ``` #### Long integers By default the library will handle automatically [BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) from Node.js v10.3 and above. If you can't use BigInts in your environment, long integers (64-bit) are also supported using the [long](https://github.com/dcodeIO/long.js) module. Example: ```javascript // => using native BigInt const stringify = fastJson({ title: 'Example Schema', type: 'object', properties: { id: { type: 'integer' } } }) const obj = { id: 18446744073709551615n } console.log(stringify(obj)) // '{"id":18446744073709551615}' // => using the long library const Long = require('long') const stringify = fastJson({ title: 'Example Schema', type: 'object', properties: { id: { type: 'integer' } } }) const obj = { id: Long.fromString('18446744073709551615', true) } console.log(stringify(obj)) // '{"id":18446744073709551615}' ``` #### Uglify If you want to squeeze a little bit more performance out of the serialization at the cost of readability in the generated code, you can pass `uglify: true` as an option. Note that you have to manually install `uglify-es` in order for this to work. Only version 3 is supported. Example: Note that if you are using Node 8.3.0 or newer, there are no performance gains from using Uglify. See https://www.nearform.com/blog/node-js-is-getting-a-new-v8-with-turbofan/ ```javascript const stringify = fastJson({ title: 'Example Schema', type: 'object', properties: { id: { type: 'integer' } } }, { uglify: true }) // stringify is now minified code console.log(stringify({ some: 'object' })) // '{"some":"object"}' ``` #### Nullable According to the [Open API 3.0 specification](https://swagger.io/docs/specification/data-models/data-types/#null), a value that can be null must be declared `nullable`. ##### Nullable object ```javascript const stringify = fastJson({ 'title': 'Nullable schema', 'type': 'object', 'nullable': true, 'properties': { 'product': { 'nullable': true, 'type': 'object', 'properties': { 'name': { 'type': 'string' } } } } }) console.log(stringify({product: {name: "hello"}})) // "{"product":{"name":"hello"}}" console.log(stringify({product: null})) // "{"product":null}" console.log(stringify(null)) // null ``` Otherwise, instead of raising an error, null values will be coerced as follows: - `integer` -> `0` - `number` -> `0` - `string` -> `""` - `boolean` -> `false` ## Caveat In order to achieve lowest cost/highest performance redaction `fast-json-stringify` creates and compiles a function (using the `Function` constructor) on initialization. While the `schema` is currently validated for any developer errors, it's recommended against allowing user input to directly supply a schema. It can't be guaranteed that allowing user input for the schema couldn't feasibly expose an attack vector. ### Debug Mode The debug mode can be activated during your development to understand what is going on when things do not work as you expect. ```js const debugCompiled = fastJson({ title: 'default string', type: 'object', properties: { firstName: { type: 'string' } } }, { debugMode: true }) console.log(debugCompiled) // it is an array of functions that can create your `stringify` function console.log(debugCompiled.toString()) // print a "ready to read" string function, you can save it to a file const rawString = debugCompiled.toString() const stringify = fastJson.restore(rawString) // use the generated string to get back the `stringify` function console.log(stringify({ firstName: 'Foo', surname: 'bar' })) // '{"firstName":"Foo"}' ``` ## Acknowledgements This project was kindly sponsored by [nearForm](http://nearform.com). ## License MIT