json-schema-to-typescript
json-schema-to-typescript copied to clipboard
Duplication of type : properties with different description
Hi there !
I have a problem with property, ref and description. If two properties of a type have the same ref definitions and two different descriptions, json2ts duplicate the type definition.
In the following case, I have a type named Offer containing two properties price and priceInclVAT of the same type Price but two different descriptions "Price excl. VAT" and "Price incl. VAT". Output from json2ts creates two types Price and Price1 and put comments over interface definitions instead of property definitions.
Source JSON Schema :
{
"title": "Offer",
"description": "An offer",
"type": "object",
"properties": {
"price": {
"description": "Price excl. VAT",
"$ref": "#/definitions/Price"
},
"priceInclVAT": {
"description": "Price incl. VAT",
"$ref": "#/definitions/Price"
}
},
"definitions": {
"Price": {
"title": "Price",
"description": "A price",
"type": "object",
"properties": {
"value": {
"description": "Price as number",
"type": "number"
},
"text": {
"description": "Price as string",
"type": "string"
}
}
}
}
}
What I obtained using json2ts :
/**
* An offer
*/
export interface Offer {
price: Price;
priceInclVAT: Price1;
}
/**
* Price excl. VAT
*/
export interface Price {
/**
* Price as number
*/
value: number;
/**
* Price as string
*/
text: string;
}
/**
* Price incl. VAT
*/
export interface Price1 {
/**
* Price as number
*/
value: number;
/**
* Price as string
*/
text: string;
}
What I expected :
/**
* An offer
*/
export interface Offer {
/**
* Price excl. VAT
*/
price: Price;
/**
* Price incl. VAT
*/
priceInclVAT: Price;
}
/**
* A price
*/
export interface Price {
/**
* Price as number
*/
value: number;
/**
* Price as string
*/
text: string;
}
Hey @maximeag! I think we'll need a few more examples from others to make a call on this one. I'll leave it open for now.
For what it's worth, since TypeScript has a structural type system, Price and Price1 are totally interchangeable (you can use a Price1 where a Price is required, and vice-versa).
I also think it makes sense to use the descriptions to annotate the fields, instead of creating new interfaces. I think that the description field in JsonSchema never actually implies a difference in schema behavior, which is why the multiple generated interfaces are interchangeable, and would be better mapped to annotations instead, as suggested.
We also don't see value in duplicated interface
I am desperately wanting an extensively documented API, so all types and all properties have descriptions; but I also don't want dupe types generated, it causes a lot of noise to consumers of my api client. Would really appreciate if this was changed so differing property descriptions do not cause new types to be generated.
For what it's worth, since TypeScript has a structural type system,
PriceandPrice1are totally interchangeable (you can use aPrice1where aPriceis required, and vice-versa).
This is not true when using ts string enums.
I.e.
{
"title": "Foo",
"type": "object",
"properties": {
"bar": {
"description": "bar",
"$ref": "#/definitions/qux"
},
"baz": {
"description": "baz",
"$ref": "#/definitions/qux"
}
},
"definitions": {
"qux": {
"title": "Qux",
"description": "A price",
"enum": ["quux", "quuz"],
"tsEnumNames": ["Quux", "Quuz"]
}
}
}
Results in:
export interface Foo {
bar?: Qux;
baz?: Qux1;
[k: string]: any;
}
/**
* bar
*/
export enum Qux {
Quux = "quux",
Quuz = "quuz"
}
/**
* baz
*/
export enum Qux1 {
Quux = "quux",
Quuz = "quuz"
}
Here, Qux and Qux1 are not interchangeable.

I agree with the sentiment described in this issue.
It's not only the 'description' field that forces 2 duplicate implementations. Also the default results in the same behavior.
I.e.
{
"title": "Foo",
"type": "object",
"properties": {
"bar": {
"$ref": "#/definitions/qux",
"default": "quuz"
},
"baz": {
"$ref": "#/definitions/qux"
}
},
"definitions": {
"qux": {
"title": "Qux",
"description": "A price",
"enum": ["quux", "quuz"],
"tsEnumNames": ["Quux", "Quuz"]
}
}
}
Results in:
export interface Foo {
bar?: Qux;
baz?: Qux1;
[k: string]: any;
}
/**
* A price
*/
export enum Qux {
Quux = "quux",
Quuz = "quuz"
}
/**
* A price
*/
export enum Qux1 {
Quux = "quux",
Quuz = "quuz"
}
A workaround is to remove all other properties (with the exception of "$ref") before generating the schema.
function preprocessSchema(inputSchema) {
switch (schemaType.type) {
case 'object':
const outputSchema = {
...inputSchema,
properties: preprocessProperties(inputSchema.properties),
definitions: preprocessProperties(inputSchema.definitions),
};
return outputSchema;
case 'array':
return {
...inputSchema,
items: preprocessSchema(inputSchema.items),
};
default:
if (inputSchema.$ref) {
// Workaround for: https://github.com/bcherny/json-schema-to-typescript/issues/193
return {
$ref: inputSchema.$ref,
};
}
if (inputSchema.allOf) {
return {
allOf: inputSchema.allOf.map(preprocessSchema),
definitions: preprocessProperties(inputSchema.definitions),
};
}
if (inputSchema.anyOf) {
return {
anyOf: inputSchema.anyOf.map(preprocessSchema),
definitions: preprocessProperties(inputSchema.definitions),
};
}
return inputSchema;
}
}
function preprocessProperties(inputProperties) {
if (inputProperties) {
const outputProperties = {};
Object.entries(inputProperties).forEach(([name, value]) => {
outputProperties[name] = preprocessSchema(value);
});
return outputProperties;
}
return undefined;
}
const schemaFile = '...';
const outFileName = '..';
const schema = preprocessSchema(require(schemaFile));
const ts = await compile(schema, schemaFile, {
style: {
singleQuote: true,
},
});
fs.writeFileSync(outFileName, ts, 'utf8');
I fixed it with #318 for internal refs to #/definitions/ only. It has a new option resolve which you have to set to false to let the $ref be handled by json-schema-to-typescript. This does mean losing external refs in that mode. I guess ideally json-schema-ref-parser would have an option to let internal $refs through.