gcanti / io-ts

Runtime type system for IO decoding/encoding
https://gcanti.github.io/io-ts/
MIT License
6.68k stars 330 forks source link

Improved JSON.stringify support for recursive types #412

Open mmkal opened 4 years ago

mmkal commented 4 years ago

🚀 Feature request

Current Behavior

JSON.stringify(...) of a recursive type doesn't have a particularly useful output.

Desired Behavior

Recursive types could emit as much information as possible when being JSON-stringified.

Suggested Solution

Here's an overly-simplistic workaround which keeps track of recursive types globally. This might break down when stringifying multiple recursive types, though.

require("fp-ts"); // fp-ts is a peer dependency. 
var t = require("io-ts")

const Category = t.recursion('Category', () =>
  t.type({
    name: t.string,
    categories: t.array(Category)
  })
)

console.log(JSON.stringify(Category, null, 2))
/* outputs:
{
  "name": "Category",
  "_tag": "RecursiveType"
}
*/

const recursiveTypes = [];
t.RecursiveType.prototype.toJSON = function() {
  const existing = recursiveTypes.find((s) => s[0] === this);
  if (existing) return existing[1];
  const pair = [this, { circular: true, name: this.name, type: { name: this.type.name, _tag: this.type._tag } }];
  recursiveTypes.push(pair);
  const result = {
    recursive: true,
    ...JSON.parse(JSON.stringify(this.type)),
  };
  pair[1] = result;
  return result;
};

console.log(JSON.stringify(Category, null, 2))
/* outputs:
{
  "recursive": true,
  "name": "Category",
  "props": {
    "name": {
      "name": "string",
      "_tag": "StringType"
    },
    "categories": {
      "name": "Array<Category>",
      "type": {
        "circular": true,
        "name": "Category",
        "type": {
          "name": "Category",
          "_tag": "InterfaceType"
        }
      },
      "_tag": "ArrayType"
    }
  },
  "_tag": "InterfaceType"
}
*/

Who does this impact? Who is this for?

Tools which take io-ts codecs and use them to auto-generate UIs or schemas in other formats - often it's useful to be able to serialise the codecs with JSON.stringify so they can be sent has HTTP request bodies, etc. Most other io-ts types retain enough information to do useful things with them.

Describe alternatives you've considered

The above prototype hack.

Additional context

Your environment

Software Version(s)
io-ts 2.0.1
fp-ts 2.4.0
TypeScript 3.7.2
mmkal commented 4 years ago

Alternatively, we could use a replacer function that doesn't require modifying the prototype:

const getReplacer = () => {
  const recursiveTypes = [];
  return function replacer(key, value) {
    const isRecursiveType = value instanceof t.RecursiveType;
    if (!isRecursiveType) {
      return value;
    }
    const existing = recursiveTypes.find((s) => s[0] === value);
    if (existing) return existing[1];
    const pair = [
      value,
      { circular: true, _tag: value._tag, name: value.name, type: { name: value.type.name, _tag: value.type._tag } },
    ];
    recursiveTypes.push(pair);
    const result = {
      recursive: true,
      ...JSON.parse(JSON.stringify(value.type, replacer)),
    };
    pair[1] = result;
    return result;
  };
};

Which allows calling JSON.stringify(Category, getReplacer()) for a similar result. It'd be nice if io-ts could supply this feature to make sure edge-cases are covered though.