json-schema-to-typescript icon indicating copy to clipboard operation
json-schema-to-typescript copied to clipboard

$ref inheritance not supported

Open JBBianchi opened this issue 1 year ago • 11 comments

When modeling inheritance, we can use $ref to point to an existing definition and extend it.

e.g.:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Sample",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "neighborhood": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/person"
      }
    }
  },
  "$defs": {
    "baseType": {
      "title": "BaseType",
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        }
      }
    },
    "person": {
      "title": "Person",
      "$ref": "#/$defs/baseType",
      "type": "object",
      "unevaluatedProperties": false,
      "properties": {
        "age": {
          "type": "number"
        }
      }
    }
  }
}

and the expected output to be equivalent to:

export interface Sample {
  neighborhood?: Person[];
}
export interface Person extends BaseType {
  age?: "number";
}
export interface BaseType {
  name?: string;
}

But with the provided code, the output of json-schema-to-typescript (14.1) with default options will be erroneous:

  • There won't be any type generated for BaseType even if it's "reachable"
  • Properties from BaseType will be missing in Person
  • Person object can have extra properties even though unevaluatedProperties is set to false

Here is the generated output with the default settings:

export interface Sample {
  neighborhood?: Person[];
}
export interface Person {
  age?: "number";
  [k: string]: unknown;
}

Even by enabling unreachableDefinitions, the output isn't better. BaseType appears but it's still not used properly.

export interface Sample {
  neighborhood?: Person[];
}
export interface Person {
  age?: "number";
  [k: string]: unknown;
}
/**
 * This interface was referenced by `Sample`'s JSON-Schema
 * via the `definition` "baseType".
 */
export interface BaseType {
  name?: string;
  [k: string]: unknown;
}
/**
 * This interface was referenced by `Sample`'s JSON-Schema
 * via the `definition` "person".
 */
export interface Person1 {
  age?: "number";
  [k: string]: unknown;
}

The closest schema that would produce something close to the expected result would be:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Sample",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "neighborhood": {
      "type": "array",
      "items": {
        "$ref": "#/$defs/person"
      }
    }
  },
  "$defs": {
    "baseType": {
      "title": "BaseType",
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        }
      }
    },
    "person": {
      "title": "Person",
      "type": "object",
      "allOf": [
        { "$ref": "#/$defs/baseType" },
        {
          "properties": {
            "age": {
              "type": "number"
            }
          },
          "unevaluatedProperties": false
        }
      ]
    }
  }
}

Edit: I removed a part I was talking about using additionalProperties to store the $ref but I was mistaken, it's not the same behavior.

JBBianchi avatar Jul 25 '24 07:07 JBBianchi

Any update on that? I have the same issue.

petrosv91 avatar Sep 09 '24 20:09 petrosv91

+1 on this bug.

We are working with pydantic in our project and they recently removed their workaround for this bug so that now we have to rebuild this workaround as long as this bug is not solved.

Here is a comment that leads to the bugfix in the json schema.

Is it possible that this issue is due to the underlying ref parser? I found this discussion and it seems it won't be solved there. From my understanding to solve this bug the ref parser needs to be changed or a user setting has to be added that allows to switch the ref parser.

noootch avatar Oct 06 '24 17:10 noootch

Excuse me, how exactly should inheritance $ref be differentiated from reuse $ref Is it that the first $ref is inheritance ref and the interface must be extended then? Or is it all refs within allOf after "type": "object" ?

bergamoticus avatar Oct 09 '24 15:10 bergamoticus

Excuse me, how exactly should inheritance $ref be differentiated from reuse $ref Is it that the first $ref is inheritance ref and the interface must be extended then? Or is it all refs within allOf after "type": "object" ?

Do you have more context about inheritance vs reuse ? I must admit I don't understand you question(s). Some samples maybe and expected output ?

JBBianchi avatar Oct 09 '24 15:10 JBBianchi

Sorry for sending unclear message. I tried to illustrated it with the example below:

bergamoticus avatar Oct 10 '24 11:10 bergamoticus

{
    "$schema": "http://json-schema.org/draft-07/schema#",
    "title": "Function",
    "type": "object",
    "additionalProperties": false,
    "allOf": [
        {
            "$ref": "#/$defs/baseType"
        },
        {
            "properties": {
                "in": {
                    "$ref": "#/$defs/taggedValue"
                },
                "out": {
                    "$ref": "#/$defs/taggedValue"
                }
            }
        }
    ],
    "$defs": {
        "baseType": {
            "title": "BaseType",
            "type": "object",
            "properties": {
                "name": {
                    "type": "string"
                }
            }
        },
        "taggedValue": {
            "title": "TaggedValue",
            "type": "object",
            "properties": {
                "tag": {
                    "type": "string"
                },
                "value": {
                    "type": "number"
                }
            }
        }
    }
}

bergamoticus avatar Oct 10 '24 11:10 bergamoticus

Actual output:

export type Function = BaseType & {
  in?: TaggedValue;
  out?: TaggedValue;
  [k: string]: unknown;
};

export interface BaseType {
  name?: string;
  [k: string]: unknown;
}
export interface TaggedValue {
  tag?: string;
  value?: number;
  [k: string]: unknown;
}

bergamoticus avatar Oct 10 '24 11:10 bergamoticus

Desired output:

export interface Function extends BaseType  {
  in?: TaggedValue;
  out?: TaggedValue;
};

export interface BaseType {
  name?: string;
}
export interface TaggedValue {
  tag?: string;
  value?: number;
}

bergamoticus avatar Oct 10 '24 11:10 bergamoticus

In the above example $ref is intended to be used for
a) extending BaseType b) shortcut/reuse TaggedValue for properties in and out. I think the correct term in UML is composition

My question is how to distinguish these different uses of ref? What is the criteria?

For instance, refs, under properties are for composition, and refs used out of properties is for inheritance (and and interface must be extended)

bergamoticus avatar Oct 10 '24 11:10 bergamoticus