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,
Price
andPrice1
are totally interchangeable (you can use aPrice1
where aPrice
is 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 $ref
s through.