APIDevTools / json-schema-ref-parser

Parse, Resolve, and Dereference JSON Schema $ref pointers in Node and browsers
https://apitools.dev/json-schema-ref-parser
MIT License
952 stars 227 forks source link

[Feature request] Support URN id references #263

Closed mikaello closed 6 months ago

mikaello commented 2 years ago

It seems that JSON Schemas with references built with URN is not supported, e.g. these schemas:

When trying to dereference these, the following error messages are produced respectively:

{
  stack: 'ResolverError: Error opening file "urn:jsonschema/:org:phenopackets:api:model:ontology:OntologyClass" \n' +
    "ENOENT: no such file or directory, open 'urn:jsonschema/:org:phenopackets:api:model:ontology:OntologyClass'\n" +
    '    at ReadFileContext.callback (/home/mikaelol/projects/temp/json-parse/node_modules/@apidevtools/json-schema-ref-parser/lib/resolvers/file.js:52:20)\n' +
    '    at FSReqCallback.readFileAfterOpen [as oncomplete] (node:fs:314:13)',
  code: 'ERESOLVER',
  message: 'Error opening file "urn:jsonschema/:org:phenopackets:api:model:ontology:OntologyClass" \n' +
    "ENOENT: no such file or directory, open 'urn:jsonschema/:org:phenopackets:api:model:ontology:OntologyClass'",
  source: 'urn:jsonschema/:org:phenopackets:api:model:ontology:OntologyClass',
  path: null,
  toJSON: [Function: toJSON],
  ioErrorCode: 'ENOENT',
  name: 'ResolverError',
  footprint: 'null+urn:jsonschema/:org:phenopackets:api:model:ontology:OntologyClass+ERESOLVER+Error opening file "urn:jsonschema/:org:phenopackets:api:model:ontology:OntologyClass" \n' +
    "ENOENT: no such file or directory, open 'urn:jsonschema/:org:phenopackets:api:model:ontology:OntologyClass'",
  toString: [Function: toString]
}

and

{
  stack: 'ResolverError: Error opening file "urn:jsonschema/:iof:v3:Country" \n' +
    "ENOENT: no such file or directory, open 'urn:jsonschema/:iof:v3:Country'\n" +
    '    at ReadFileContext.callback (/home/mikaelol/projects/temp/json-parse/node_modules/@apidevtools/json-schema-ref-parser/lib/resolvers/file.js:52:20)\n' +
    '    at FSReqCallback.readFileAfterOpen [as oncomplete] (node:fs:314:13)',
  code: 'ERESOLVER',
  message: 'Error opening file "urn:jsonschema/:iof:v3:Country" \n' +
    "ENOENT: no such file or directory, open 'urn:jsonschema/:iof:v3:Country'",
  source: 'urn:jsonschema/:iof:v3:Country',
  path: null,
  toJSON: [Function: toJSON],
  ioErrorCode: 'ENOENT',
  name: 'ResolverError',
  footprint: 'null+urn:jsonschema/:iof:v3:Country+ERESOLVER+Error opening file "urn:jsonschema/:iof:v3:Country" \n' +
    "ENOENT: no such file or directory, open 'urn:jsonschema/:iof:v3:Country'",
  toString: [Function: toString]
}
jonluca commented 6 months ago

I'm not sure this is straightforward to support generically - I'd recommend writing your own custom resolver for the URN namespaces you want to support

mikaello commented 6 months ago

Thanks for getting back to me. In JVM world it is not that troublesome to support generically by leveraging the Jackson library, here is an example I have written in Kotlin (see parseReference function at the bottom):

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.databind.node.TextNode

private val typeMap = mutableMapOf<String, String>()

private fun parseReferences(jsonNode: JsonNode, path: String) {
    val ITEMS = "items"
    val ID = "id"
    val PROPERTIES = "properties"
    val ADDITIONAL_PROPERTIES = "additionalProperties"
    val REF = "\$ref"

    var currPath = path

    if (jsonNode.has(ID)) {
        typeMap.put(jsonNode.get(ID).asText(), currPath)
        val properties: JsonNode = jsonNode.get(PROPERTIES)
        val fields: Iterator<Map.Entry<String, JsonNode>> = properties.fields()
        currPath += "/$PROPERTIES"
        while (fields.hasNext()) {
            val entry = fields.next()
            parseReferences(entry.value, currPath + "/" + entry.key)
        }
    } else if (jsonNode.has(ITEMS)) {
        val item: JsonNode = jsonNode.get(ITEMS)
        parseReferences(item, "$currPath/$ITEMS")
    } else if (jsonNode.has(REF)) {
        val objectNode = jsonNode as ObjectNode
        objectNode.set(REF, TextNode(typeMap.get(jsonNode.get(REF).asText())))
    } else if (jsonNode.has(ADDITIONAL_PROPERTIES)) {
        val additionalProperties: JsonNode = jsonNode.get(ADDITIONAL_PROPERTIES)
        parseReferences(additionalProperties, "$currPath/$ADDITIONAL_PROPERTIES")
    }
}

/**
 * Remove URN from JSON, and replace with # type references
 */
fun parseReference(json: String): String {
    val jaxbObjectMapper = ObjectMapper()
    val root: JsonNode = jaxbObjectMapper.readTree(json)
    parseReferences(root, "#")
    return jaxbObjectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(root)
}

So that is why I thought this should be doable without to much trouble in TypeScript as well. URNs are valid IDs in JSON Schema, so I guess this affects others as well.