jsonunit-testingnjsonschema

Resolve JSON $ref in unit tests using NJsonSchema


I have split my JSON schema into several files and reference them as required in the standard way ("$ref": http://rootpath/otherfile.json#/definitions/link).

The JSON files are embedded resources within the project. The rootpath changes depending on where it is deployed. But in production everything works fine (a request is made the JSON response is obtained as is the schema, when validating the response against the schema NJsonSchema internally get the reference schema(s) and extracts what it requires to complete the validation)

When it comes to testing, however, it is a different matter. The response is fine, and get the first schema is stubbed out. The rootpath is such that everything is relative to http://testapi/ but this doesn't actually exist. So when NJsonSchema attempt to get a reference schema it looks up something like http://testapi/otherfile.json#/definitions/link, which obviously fails.

From reading this I think I'm wanting to use an overload to get the JsonSchema4 object that allows me to specify the JsonReferenceResolver, then I could use the default in production and inject my own one for testing such that it looks for the $refs somewhere that I control and will exist. But I can't see any documentation or examples on this.

Example schemas:

{ // root.json
    "$schema": "http://json-schema.org/draft-07/schema#",
    "$id": "@Model.Root/schemas/root.json",
    "title": "Root",
    "properties": {
        "link": { "$ref": "@Model.Root/schemas/common.json#/definitions/link" }             
    },
    "required": [ "link" ]
}

{ // common.json - different file to the above
    "$schema": "http://json-schema.org/draft-07/schema#",
    "$id": "@Model.Root/schemas/common.json",
    "definitions": {
        "link": {
            "title": "Link",
            "type": "object",
            "properties": {
                "rel": { "type": "string" },
                "href": { "type": "string" }
            },
            "required": [ "rel", "href" ]
        }
    }
}

Example response:

{
    "schema": "http://testapi/schemas/root.json",
    "link": { "rel": "self", "href": "http://testapi/root" }
};

Validation code (C#):

using NJsonSchema;
using NJsonSchema.Validation;
...
JsonSchema4 schema = await JsonSchema4.FromJsonAsync(<contents of root.json file>);
string response = "{ ""link"": { ""rel"": ""self"", ""href"": ""http://testapi/root"" } }";
ICollection<ValidationError> errors = schema.Validate(response);
...

Solution

  • My hunch of specifing the JsonReferenceResolver was on the right line. In the above example I couldn't get the reference to common.json from root.json to 'work' in a test environment, because in that environment the service/path didn't exist.

    So when setting up the environment I do know what common.json is i.e. I know it's contents as a string and can therefore get it as a schema (JsonSchema4 commonSchema = JsonSchema4.FromJsonAsync(commonJsonAsString).Result;).

    Using this I can set up a 'Reference Resolver Factory' and within it add a document reference such that all references to http://testapi/schemas/common.json#/... are obtained from my commonSchema i.e.

    private Func<JsonSchema4, JsonReferenceResolver> referenceResolverFactory;
    this.referenceResolverFactory = x =>
    {
        JsonSchemaResolver schemaResolver = new JsonSchemaResolver(x, new JsonSchemaGeneratorSettings());
        JsonReferenceResolver referenceResolver = new JsonReferenceResolver(schemaResolver);
        referenceResolver.AddDocumentReference("http://testapi/schemas/common.json", commonSchema);
    
        return referenceResolver;
    };
    

    Different references can be resolved by adding more document references in the same way.

    Then when I try to convert the root.json schema (with it's reference to common.json) or another schema (that references common.json) I use an overload of FromJsonAsync which passes in the referenceResolverFactory:

    private bool GetAndValidateResponse(string schemaName, string response, string schemaAsString)
    {
        // schemaName:     Name of the schema being validated, e.g. "root.json"
        // response:       http response body as a string, see 'Example response' in question
        // schemaAsString: The schema being validate as a string, e.g. root.json contents in question
        //
        // Check the response object against the schema
        JsonSchema4 schema = JsonSchema4.FromJsonAsync(schemaAsString, schemaName, this.referenceResolverFactory).Result;
        ICollection<ValidationError> errors = schema.Validate(response);
    
        return errors.Count < 1;
    }
    

    In my case for the unit tests I was only interested in it being valid or not, hence the boolean return value. If you are interested in the exact errors obviously you could return the collection.

    Also I only set up the factory once per unit test class, rather than per test. However, it rightly goes in to the factory on each for each ref in a schema.