gregsdennis / Manatee.Json

A fully object-oriented approach to JSON manipulation, validation, and serialization that focuses on modeling the JSON structure rather than mere string parsing and conversion.
MIT License
198 stars 32 forks source link

Easier way to do this? #264

Closed kentcb closed 4 years ago

kentcb commented 4 years ago

This is a question rather than a bug or feature request, but it may evolve into either.

I wrote the following code to illustrate my problem:

            var schemaText = @"{
    ""$schema"": ""http://json-schema.org/draft-07/schema"",
    ""definitions"": {
        ""first"": {
            ""$id"": ""#first"",
            ""properties"": {
                ""foo"": {
                    ""type"": ""string""
                }
            },
            ""required"": [
                ""foo""
            ]
        },
        ""second"": {
            ""$id"": ""#first"",
            ""properties"": {
                ""bar"": {
                    ""type"": ""string""
                }
            },
            ""required"": [
                ""bar""
            ]
        }
    },
    ""type"": ""object"",
    ""$ref"": ""#first""
}";

            var schemaJson = JsonValue.Parse(schemaText);
            var schema = new JsonSerializer().Deserialize<JsonSchema>(schemaJson);

            var definitions = schema.First(x => x.Name == "definitions") as DefinitionsKeyword;
            var second = definitions["second"] as JsonSchema;

            var input = @"
{
    ""bar"": ""hello""
}
";
            var doc = JsonValue.Parse(input);
            var result = second.Validate(doc);

            if (result.IsValid)
            {
                return;
            }

As you can see, I'm trying to resolve a schema within a schema so I can then go on to validate using it. My initial attempts at this revolved around using schema.ResolveSubschema(JsonPointer.Parse("#/definitions/second"), new Uri("", UriKind.Relative)) basically because that felt like the right path to be going down. However, I simply could not get it to work.

So I'm wondering whether there's a nicer/cleaner way of achieving this?

gregsdennis commented 4 years ago

In your example, both definitions have an $id of #first. This may be a typo, but it'll definitely not work that way. I'll assume that you mean #second for the second definition, as indicated by your question.

I'm going to file this under "use cases I never considered". I'm guessing that you're actually loading a schema from disk and you want a one of its subschemas.

Do you have control over that schema file? If so, I'd suggest breaking it into multiple schemas rather than loading one and trying to pick out a subschema.

Schema 1

{
    "$schema": "http://json-schema.org/draft-07/schema",
    "definitions": {
        "first": {
            "$id": "#first",
            "properties": {
                "foo": {
                    "type": "string"
                }
            },
            "required": [
                "foo"
            ]
        }
    },
    "type": "object",
    "$ref": "file:///c:\\schemas\\second.json"  // or whatever
}

Schema 2

{
    "$id": "#first",
    "properties": {
        "bar": {
            "type": "string"
        }
    },
    "required": [
        "bar"
    ]
}

If that's not possible then you're pretty much better off doing what you're doing, though getting to the definitions can be a little simpler by using the extension method:

var second = schema.Definitions()["second"];

I'd avoid the ResolveSubschema() method. That's designed for keywords to be able to resolve references. The only reason it's public is because the library is designed so that users can define their own keywords and may need this functionality.

kentcb commented 4 years ago

Thanks for the quick response.

Yeah, that was a typo sorry. Also, whilst my hack works in the case of my simple repro above, it falls apart when the sub-schema itself uses $ref as in this example:

{
    "$schema": "http://json-schema.org/draft-07/schema",
    "definitions": {
        "something": {
            "$id": "#something",
            "properties": {
                "id": {
                    "$ref": "#uuid"
                }
            },
            "required": [
                "name"
            ]
        },
        "something_else": {
            "$id": "#something_else",
            "properties": {
                "foo": {
                    "type": "string"
                }
            },
            "required": [
                "foo"
            ]
        },
        "uuid": {
            "$id": "#uuid",
            "type": "string",
            "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
        }
    },
    "type": "object",
    "$ref": "#something"
}

Attempting to resolve the second schema and validate against it fails with:

System.ArgumentOutOfRangeException
  HResult=0x80131502
  Message=Specified argument was out of the range of valid values. 
  Source=System.Private.Uri
  StackTrace:
   at System.Uri..ctor(Uri baseUri, Uri relativeUri)
   at Manatee.Json.Schema.JsonSchema.ResolveSubschema(JsonPointer pointer, Uri baseUri) in C:\Users\Kent\Repository\Manatee.Json\Manatee.Json\Schema\JsonSchema.cs:line 262
   at Manatee.Json.Schema.RefKeyword._ResolveLocalReference(Uri baseUri) in C:\Users\Kent\Repository\Manatee.Json\Manatee.Json\Schema\Keywords\RefKeyword.cs:line 225
   at Manatee.Json.Schema.RefKeyword._ResolveReference(SchemaValidationContext context) in C:\Users\Kent\Repository\Manatee.Json\Manatee.Json\Schema\Keywords\RefKeyword.cs:line 214
   at Manatee.Json.Schema.RefKeyword.Validate(SchemaValidationContext context) in C:\Users\Kent\Repository\Manatee.Json\Manatee.Json\Schema\Keywords\RefKeyword.cs:line 72
   at Manatee.Json.Schema.JsonSchema.Validate(SchemaValidationContext context) in C:\Users\Kent\Repository\Manatee.Json\Manatee.Json\Schema\JsonSchema.cs:line 328
   at Manatee.Json.Schema.PropertiesKeyword.Validate(SchemaValidationContext context) in C:\Users\Kent\Repository\Manatee.Json\Manatee.Json\Schema\Keywords\PropertiesKeyword.cs:line 78
   at Manatee.Json.Schema.JsonSchema.Validate(SchemaValidationContext context) in C:\Users\Kent\Repository\Manatee.Json\Manatee.Json\Schema\JsonSchema.cs:line 328
   at Manatee.Json.Schema.JsonSchema.Validate(JsonValue json) in C:\Users\Kent\Repository\Manatee.Json\Manatee.Json\Schema\JsonSchema.cs:line 192
   at Harness.Program.Main(String[] args) in C:\Users\Kent\Repository\Manatee.Json\Harness\Program.cs:line 25
gregsdennis commented 4 years ago

That's because the definitions (and all other keywords) are ignored when they are sibling to a $ref in draft 7 and below. You'll want to wrap the $ref in an allOf. (I didn't see that before.)

gregsdennis commented 4 years ago

@kentcb did I answer your question?

kentcb commented 4 years ago

@gregsdennis I never did get it to work, even when trying to wrap in allOf. I gave up due to time pressure and did a hacky solution instead. Happy to close this anyway for now since I don't intend coming back to it just yet. Thanks Greg.