typescript-json-schema
typescript-json-schema copied to clipboard
[feature request] replace repeated nested structures with references
Sorry if this behavior already exists, I've reviewed the docs and am pretty sure it doesn't. I'm transferring ts -> json schema and then json schema -> pydantic. In this process the naming is important because otherwise some types will lose their meaning. Currently, it is challenging for the json schema -> pydantic model to resolve nested common refs. The --aliasRefs seems to address this issue nicely, but it does abstract the names of the refs as well. For instance,
{
"ISectionBlockMetadata": {
"$ref": "#/definitions/__type_2"
},
"__type_2": {
"properties": {
"asText": {
"title": "asText",
"type": "string"
}
},
"required": [
"asText"
],
"title": "__type_2",
"type": "object"
},
}
In this case the definition of __type_2 and its name ISectionBlockMetadata are separated. When it's compiled to pydantic it converts to something like:
class ISectionBlockMetadata(BaseModel):
__root__: FieldType2
class FieldType2(BaseModel):
as_text: str = Field(..., alias='asText', title='asText')
I believe a possible workaround for this could be to simply pass the parent ref name to the child as the title. For instance, I'd expect it to result in the following:
{
"ISectionBlockMetadata": {
"$ref": "#/definitions/__type_2"
},
"__type_2": {
"properties": {
"asText": {
"title": "asText",
"type": "string"
}
},
"required": [
"asText"
],
"title": "ISectionBlockMetadata",
"type": "object"
},
}
I messed around with it and was able to get the behavior I want. To do this I have to run two javascript files, one after the other. Here's how I did it:
const fs = require('fs');
fs.readFile('./out/generated.json', 'utf8', (err, jsonString) => {
if (err) {
console.log("File read failed:", err);
return;
}
const data = JSON.parse(jsonString);
// Make sure there is a definitions object
if (!data.definitions) {
console.log("No definitions found in JSON file.");
return;
}
const refCount = {};
// First pass: count all references
Object.values(data.definitions).forEach((definition) => {
// Check if definition is an object
if (typeof definition === 'object' && definition !== null && definition.hasOwnProperty('$ref')) {
const refName = definition['$ref'].split("/").pop();
if(refCount[refName]) {
refCount[refName]++;
} else {
refCount[refName] = 1;
}
}
});
// Second pass: rename reference and delete old one if it's used only once
Object.keys(data.definitions).forEach((key) => {
const definition = data.definitions[key];
// Check if definition is an object
if (typeof definition === 'object' && definition !== null && definition.hasOwnProperty('$ref')) {
const refName = definition['$ref'].split("/").pop();
if(refCount[refName] === 1) {
data.definitions[key] = data.definitions[refName];
data.definitions[key].title = key;
delete data.definitions[refName];
}
}
});
fs.writeFile('./out/generated.json', JSON.stringify(data, null, 2), 'utf8', function(err) {
if(err) {
console.log("An error occurred while writing JSON Object to file.");
return console.log(err);
}
console.log("JSON file has been saved with updated titles.");
});
});
then,
const fs = require('fs');
const _ = require('lodash');
function replaceNestedObjectsWithRefs(jsonObj) {
const definitions = jsonObj.definitions;
const isObjectEqual = (object1, object2) => {
// Exclude the "title" property for comparison
const { title: __, ...object1WithoutTitle } = object1;
const { title: ___, ...object2WithoutTitle } = object2;
return _.isEqual(object1WithoutTitle, object2WithoutTitle);
};
for (const key in definitions) {
const def = definitions[key];
_.forIn(def, function process(value, key, object) {
if (_.isObject(value)) {
for (const defKey in definitions) {
if (isObjectEqual(value, definitions[defKey])) {
object[key] = { "$ref": `#/definitions/${defKey}` };
} else {
_.forIn(value, process);
}
}
}
});
}
return jsonObj;
}
fs.readFile('./out/generated.json', 'utf8', (err, data) => {
if (err) {
console.error('Error reading file:', err);
return;
}
const jsonObj = JSON.parse(data);
const updatedJsonObj = replaceNestedObjectsWithRefs(jsonObj);
fs.writeFile('schema.json', JSON.stringify(updatedJsonObj, null, 2), (err) => {
if (err) {
console.error('Error writing file:', err);
}
});
});
note that this is a hacky solution, but maybe it'll be of use to aid in explaining the request?
I needed to optimize the above because it was very slow, here's the updated version, I also moved it to typescript and wrote tests for it. You can see the code in this gist: https://gist.github.com/alita-moore/a2ce9b9ceefcf9fa9f14a965f76fe015
edit: I made a change to sort the keys in the objects before checking for repeats. That way it improves accuracy.