ts-json-schema-generator icon indicating copy to clipboard operation
ts-json-schema-generator copied to clipboard

Incorrect schema generation when using recursive mapped types

Open julienvincent opened this issue 4 years ago • 2 comments

Hi there! I have run into a few bugs while trying to generate schemas using recursive mapped types.

  • Unions are incorrectly merged into a single definition
  • Comments are lost/replaced with comment from the mapped type
  • Property modifiers are sometimes lost (like readonly, optional) however I can't yet properly reproduce

Given the following set of types:

type A = {
  /**
   * Some comment on property a
   */
  a: string
}

type B = {
  /**
   * Some comment on property b
   */
  b?: string
  y: string
}

type ComposedWithIntersection = (A | B) & {
  model: string
}

type ComposedSingle = A | B

export type Parent = {
  prop: string
  composed_single: ComposedSingle[]
  composed_with_intersection: ComposedWithIntersection
}

type Primitive = string | number | boolean | null | undefined
type ComplexProperty = Date

/**
 * Comment on mapped type. Should not appear on usages
 */
type SerializedDeep<T> = T extends Primitive
  ? T
  : T extends ComplexProperty
  ? string
  : {
      [P in keyof T]: SerializedDeep<T[P]>
    }

export type SerializedParent = SerializedDeep<Parent>

This will produce the following incorrect schema:

Generated Schema
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "definitions": {
    "Parent": {
      "additionalProperties": false,
      "properties": {
        "composed_single": {
          "items": {
            "anyOf": [
              {
                "additionalProperties": false,
                "properties": {
                  "a": {
                    "description": "Some comment on property a",
                    "type": "string"
                  }
                },
                "required": ["a"],
                "type": "object"
              },
              {
                "additionalProperties": false,
                "properties": {
                  "b": {
                    "description": "Some comment on property b",
                    "type": "string"
                  },
                  "y": {
                    "type": "string"
                  }
                },
                "required": ["y"],
                "type": "object"
              }
            ]
          },
          "type": "array"
        },
        "composed_with_intersection": {
          "anyOf": [
            {
              "additionalProperties": false,
              "properties": {
                "a": {
                  "description": "Some comment on property a",
                  "type": "string"
                },
                "model": {
                  "type": "string"
                }
              },
              "required": ["a", "model"],
              "type": "object"
            },
            {
              "additionalProperties": false,
              "properties": {
                "b": {
                  "description": "Some comment on property b",
                  "type": "string"
                },
                "model": {
                  "type": "string"
                },
                "y": {
                  "type": "string"
                }
              },
              "required": ["model", "y"],
              "type": "object"
            }
          ]
        },
        "prop": {
          "type": "string"
        }
      },
      "required": ["prop", "composed_single", "composed_with_intersection"],
      "type": "object"
    },
    "SerializedParent": {
      "additionalProperties": false,
      "description": "Comment on mapped type. Should not appear on usages",
      "properties": {
        "composed_single": {
          "description": "Comment on mapped type. Should not appear on usages",
          "items": {
            "additionalProperties": false,
            "description": "Comment on mapped type. Should not appear on usages",
            "properties": {
              "a": {
                "description": "Comment on mapped type. Should not appear on usages",
                "type": "string"
              },
              "b": {
                "description": "Comment on mapped type. Should not appear on usages",
                "type": "string"
              },
              "y": {
                "description": "Comment on mapped type. Should not appear on usages",
                "type": "string"
              }
            },
            "required": ["a", "y"],
            "type": "object"
          },
          "type": "array"
        },
        "composed_with_intersection": {
          "additionalProperties": false,
          "description": "Comment on mapped type. Should not appear on usages",
          "properties": {
            "a": {
              "description": "Comment on mapped type. Should not appear on usages",
              "type": "string"
            },
            "b": {
              "description": "Comment on mapped type. Should not appear on usages",
              "type": "string"
            },
            "model": {
              "description": "Comment on mapped type. Should not appear on usages",
              "type": "string"
            },
            "y": {
              "description": "Comment on mapped type. Should not appear on usages",
              "type": "string"
            }
          },
          "required": ["a", "model", "y"],
          "type": "object"
        },
        "prop": {
          "description": "Comment on mapped type. Should not appear on usages",
          "type": "string"
        }
      },
      "required": ["prop", "composed_single", "composed_with_intersection"],
      "type": "object"
    }
  }
}

Here you can compare the generation of Parent and SerializedParent. You will see that SerializedParent's composed_single and composed_with_intersection properties have merged their unions in an incorrect manner.

Additionally, the comments for property a and property b have been lost and all properties have inherited the comment from the mapped type.

I also want to report having run into an issue with optional properties having their optionality lost (they would become required in the schema) however I haven't been able to successfully reproduce - so I will update this issue once I can. I reworked the mapped type to fix this issue and cannot remember what it was originally.


I would love to help contribute to fixing this issue, but reading through the code it is way over my head :)

Similar issue: https://github.com/vega/ts-json-schema-generator/issues/126

julienvincent avatar Sep 19 '21 13:09 julienvincent

From digging in the code a bit it seems the union issue is somewhere around here:

https://github.com/vega/ts-json-schema-generator/blob/next/src/NodeParser/MappedTypeNodeParser.ts#L38

As this function incorrectly returns the merged types. But I don't understand enough to see in what way this should change

julienvincent avatar Sep 19 '21 15:09 julienvincent

Would it be possible for someone to take a look at this?

julienvincent avatar Nov 11 '21 13:11 julienvincent