diff --git a/src/NJsonSchema.Tests/Schema/JsonSchemaTests.cs b/src/NJsonSchema.Tests/Schema/JsonSchemaTests.cs index 9a1eec633..1222cd70d 100644 --- a/src/NJsonSchema.Tests/Schema/JsonSchemaTests.cs +++ b/src/NJsonSchema.Tests/Schema/JsonSchemaTests.cs @@ -427,6 +427,114 @@ public async Task When_azure_schema_is_loaded_then_no_exception() Assert.Contains("The identity type.", json); } + [Fact] + public void When_FromJson_is_called_with_simple_schema_then_it_is_deserialized() + { + //// Arrange + var data = +@"{ + ""title"": ""Test Schema"", + ""type"": ""object"", + ""properties"": { + ""name"": { + ""type"": ""string"" + }, + ""age"": { + ""type"": ""integer"" + } + }, + ""required"": [""name""] +}"; + + //// Act + var schema = JsonSchema.FromJson(data); + + //// Assert + Assert.Equal("Test Schema", schema.Title); + Assert.Equal(JsonObjectType.Object, schema.Type); + Assert.Equal(2, schema.Properties.Count); + Assert.True(schema.Properties.ContainsKey("name")); + Assert.True(schema.Properties.ContainsKey("age")); + Assert.Contains("name", schema.RequiredProperties); + } + + [Fact] + public void When_FromJson_is_called_with_internal_refs_then_they_are_resolved() + { + //// Arrange + var data = +@"{ + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""type"": ""object"", + ""properties"": { + ""address"": { + ""$ref"": ""#/definitions/Address"" + } + }, + ""definitions"": { + ""Address"": { + ""type"": ""object"", + ""properties"": { + ""street"": { ""type"": ""string"" }, + ""city"": { ""type"": ""string"" } + } + } + } +}"; + + //// Act + var schema = JsonSchema.FromJson(data); + + //// Assert + Assert.NotNull(schema.Definitions["Address"]); + Assert.NotNull(schema.Properties["address"].Reference); + Assert.Equal(schema.Definitions["Address"], schema.Properties["address"].Reference); + } + + [Fact] + public async Task When_FromJson_is_called_then_result_matches_FromJsonAsync() + { + //// Arrange + var data = +@"{ + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""type"": ""object"", + ""properties"": { + ""storage"": { + ""type"": ""object"", + ""oneOf"": [ + { ""$ref"": ""#/definitions/diskDevice"" }, + { ""$ref"": ""#/definitions/diskUUID"" } + ] + }, + ""fstype"": { + ""enum"": [ ""ext3"", ""ext4"", ""btrfs"" ] + } + }, + ""definitions"": { + ""diskDevice"": { + ""type"": ""object"", + ""properties"": { + ""device"": { ""type"": ""string"" } + } + }, + ""diskUUID"": { + ""type"": ""object"", + ""properties"": { + ""uuid"": { ""type"": ""string"" } + } + } + } +}"; + + //// Act + var syncSchema = JsonSchema.FromJson(data); + var asyncSchema = await JsonSchema.FromJsonAsync(data); + + //// Assert + Assert.Equal(asyncSchema.ToJson(), syncSchema.ToJson()); + } + [Fact] public void When_there_are_multiple_empty_objects_in_json_sample_data_generate_different_type_definitions_foreach() { diff --git a/src/NJsonSchema/Infrastructure/JsonSchemaSerialization.cs b/src/NJsonSchema/Infrastructure/JsonSchemaSerialization.cs index 7423ad69b..26f460bf1 100644 --- a/src/NJsonSchema/Infrastructure/JsonSchemaSerialization.cs +++ b/src/NJsonSchema/Infrastructure/JsonSchemaSerialization.cs @@ -124,6 +124,49 @@ public static Task FromJsonAsync(Stream stream, SchemaType schemaType, str return FromJsonWithLoaderAsync(loader, schemaType, documentPath, referenceResolverFactory, contractResolver, cancellationToken); } + /// Deserializes JSON data to a schema with reference handling (synchronous version). + /// Only supports document-internal references (# and #/...). External file or URL + /// references will throw . + /// The JSON data. + /// The schema type. + /// The document path. + /// The reference resolver factory. + /// The contract resolver. + /// The deserialized schema. + public static T FromJson(string json, SchemaType schemaType, string? documentPath, + Func referenceResolverFactory, IContractResolver contractResolver) + where T : notnull + { + CurrentSchemaType = schemaType; + + T schema; + try + { + schema = FromJson(json, contractResolver)!; + if (schema is IDocumentPathProvider documentPathProvider) + { + documentPathProvider.DocumentPath = documentPath; + } + + var referenceResolver = referenceResolverFactory.Invoke(schema); + if (schema is IJsonReference referenceSchema) + { + if (!string.IsNullOrEmpty(documentPath)) + { + referenceResolver.AddDocumentReference(documentPath!, referenceSchema); + } + } + + JsonSchemaReferenceUtilities.UpdateSchemaReferences(schema, referenceResolver, contractResolver); + } + finally + { + CurrentSchemaType = SchemaType.JsonSchema; + } + + return schema; + } + private static async Task FromJsonWithLoaderAsync( Func loader, SchemaType schemaType, diff --git a/src/NJsonSchema/JsonReferenceResolver.cs b/src/NJsonSchema/JsonReferenceResolver.cs index ea2c17934..468af6620 100644 --- a/src/NJsonSchema/JsonReferenceResolver.cs +++ b/src/NJsonSchema/JsonReferenceResolver.cs @@ -103,6 +103,59 @@ public virtual IJsonReference ResolveDocumentReference(object rootObject, string return schema; } + /// Gets the object from the given JSON path (synchronous version). + /// Only supports document-internal references (# and #/...). External file or URL + /// references will throw . + /// The root object. + /// The JSON path. + /// The target type to resolve. + /// The contract resolver. + /// The JSON Schema. + /// Could not resolve the JSON path. + /// External file or URL references are not supported in synchronous mode. + public IJsonReference ResolveReference(object rootObject, string jsonPath, Type targetType, IContractResolver contractResolver) + { + return ResolveReference(rootObject, jsonPath, targetType, contractResolver, true); + } + + /// Gets the object from the given JSON path without appending (synchronous version). + /// Only supports document-internal references (# and #/...). External file or URL + /// references will throw . + /// The root object. + /// The JSON path. + /// The target type to resolve. + /// The contract resolver. + /// The JSON Schema. + /// Could not resolve the JSON path. + /// External file or URL references are not supported in synchronous mode. + public IJsonReference ResolveReferenceWithoutAppend(object rootObject, string jsonPath, Type targetType, IContractResolver contractResolver) + { + return ResolveReference(rootObject, jsonPath, targetType, contractResolver, false); + } + + private IJsonReference ResolveReference(object rootObject, string jsonPath, Type targetType, IContractResolver contractResolver, bool append) + { + if (jsonPath == "#") + { + if (rootObject is IJsonReference) + { + return (IJsonReference)rootObject; + } + + throw new InvalidOperationException("Could not resolve the JSON path '#' because the root object is not a JsonSchema4."); + } + else if (jsonPath.StartsWith("#/")) + { + return ResolveDocumentReference(rootObject, jsonPath, targetType, contractResolver); + } + else + { + throw new NotSupportedException( + "Could not resolve the JSON path '" + jsonPath + "' synchronously. " + + "External file and URL references require the async FromJsonAsync method."); + } + } + /// Resolves a file reference. /// The file path. /// The cancellation token diff --git a/src/NJsonSchema/JsonSchema.cs b/src/NJsonSchema/JsonSchema.cs index 99d493d48..63de11c67 100644 --- a/src/NJsonSchema/JsonSchema.cs +++ b/src/NJsonSchema/JsonSchema.cs @@ -189,6 +189,41 @@ public static Task FromJsonAsync(Stream stream, string? documentPath return JsonSchemaSerialization.FromJsonAsync(stream, SerializationSchemaType, documentPath, referenceResolverFactory, ContractResolver.Value, cancellationToken); } + /// Deserializes a JSON string to a (synchronous version). + /// Only supports document-internal references (# and #/...). External file or URL + /// references will throw . + /// The JSON string. + /// The JSON Schema. + public static JsonSchema FromJson(string data) + { + return FromJson(data, null); + } + + /// Deserializes a JSON string to a (synchronous version). + /// Only supports document-internal references (# and #/...). External file or URL + /// references will throw . + /// The JSON string. + /// The document path (URL or file path) for resolving relative document references. + /// The JSON Schema. + public static JsonSchema FromJson(string data, string? documentPath) + { + var factory = JsonReferenceResolver.CreateJsonReferenceResolverFactory(new DefaultTypeNameGenerator()); + return FromJson(data, documentPath, factory); + } + + /// Deserializes a JSON string to a (synchronous version). + /// Only supports document-internal references (# and #/...). External file or URL + /// references will throw . + /// The JSON string. + /// The document path (URL or file path) for resolving relative document references. + /// The JSON reference resolver factory. + /// The JSON Schema. + public static JsonSchema FromJson(string data, string? documentPath, Func referenceResolverFactory) + { + return JsonSchemaSerialization.FromJson(data, SerializationSchemaType, documentPath, referenceResolverFactory, ContractResolver.Value); + } + /// Creates a from a given type (using System.Text.Json rules). /// The type to create the schema for. /// The . diff --git a/src/NJsonSchema/JsonSchemaReferenceUtilities.cs b/src/NJsonSchema/JsonSchemaReferenceUtilities.cs index c29941498..3aee04393 100644 --- a/src/NJsonSchema/JsonSchemaReferenceUtilities.cs +++ b/src/NJsonSchema/JsonSchemaReferenceUtilities.cs @@ -41,6 +41,29 @@ public static async Task UpdateSchemaReferencesAsync(object rootObject, JsonRefe await updater.VisitAsync(rootObject, cancellationToken).ConfigureAwait(false); } + /// Updates all properties from the + /// available properties (synchronous version). + /// Only supports document-internal references (# and #/...). External file or URL + /// references will throw . + /// The root object. + /// The JSON document resolver. + public static void UpdateSchemaReferences(object rootObject, JsonReferenceResolver referenceResolver) => + UpdateSchemaReferences(rootObject, referenceResolver, new DefaultContractResolver()); + + /// Updates all properties from the + /// available properties (synchronous version). + /// Only supports document-internal references (# and #/...). External file or URL + /// references will throw . + /// The root object. + /// The JSON document resolver. + /// The contract resolver. + public static void UpdateSchemaReferences(object rootObject, JsonReferenceResolver referenceResolver, + IContractResolver contractResolver) + { + var updater = new JsonReferenceUpdaterSync(rootObject, referenceResolver, contractResolver); + updater.Visit(rootObject); + } + /// Updates the properties /// from the available properties with inlining external references. /// The root object. @@ -121,6 +144,54 @@ protected override async Task VisitJsonReferenceAsync(IJsonRefer } } + private sealed class JsonReferenceUpdaterSync : JsonReferenceVisitorBase + { + private readonly object _rootObject; + private readonly JsonReferenceResolver _referenceResolver; + private readonly IContractResolver _contractResolver; + private bool _replaceRefsRound; + + public JsonReferenceUpdaterSync(object rootObject, JsonReferenceResolver referenceResolver, IContractResolver contractResolver) + : base(contractResolver) + { + _rootObject = rootObject; + _referenceResolver = referenceResolver; + _contractResolver = contractResolver; + } + + public override void Visit(object obj) + { + _replaceRefsRound = true; + base.Visit(obj); + _replaceRefsRound = false; + base.Visit(obj); + } + + protected override IJsonReference VisitJsonReference(IJsonReference reference, string path, string? typeNameHint) + { + if (reference.ReferencePath != null && reference.Reference == null) + { + if (_replaceRefsRound) + { + if (path.EndsWith("/definitions/" + typeNameHint) || path.EndsWith("/schemas/" + typeNameHint)) + { + // inline $refs in "definitions" + return _referenceResolver + .ResolveReferenceWithoutAppend(_rootObject, reference.ReferencePath, reference.GetType(), _contractResolver); + } + } + else + { + // load $refs and add them to "definitions" + reference.Reference = _referenceResolver + .ResolveReference(_rootObject, reference.ReferencePath, reference.GetType(), _contractResolver); + } + } + + return reference; + } + } + private sealed class JsonReferencePathUpdater : JsonReferenceVisitorBase { private readonly object _rootObject;