Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions src/NJsonSchema.Tests/Schema/JsonSchemaTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
43 changes: 43 additions & 0 deletions src/NJsonSchema/Infrastructure/JsonSchemaSerialization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,49 @@ public static Task<T> FromJsonAsync<T>(Stream stream, SchemaType schemaType, str
return FromJsonWithLoaderAsync(loader, schemaType, documentPath, referenceResolverFactory, contractResolver, cancellationToken);
}

/// <summary>Deserializes JSON data to a schema with reference handling (synchronous version).
/// Only supports document-internal references (# and #/...). External file or URL
/// references will throw <see cref="NotSupportedException"/>.</summary>
/// <param name="json">The JSON data.</param>
/// <param name="schemaType">The schema type.</param>
/// <param name="documentPath">The document path.</param>
/// <param name="referenceResolverFactory">The reference resolver factory.</param>
/// <param name="contractResolver">The contract resolver.</param>
/// <returns>The deserialized schema.</returns>
public static T FromJson<T>(string json, SchemaType schemaType, string? documentPath,
Func<T, JsonReferenceResolver> referenceResolverFactory, IContractResolver contractResolver)
where T : notnull
{
CurrentSchemaType = schemaType;

T schema;
try
{
schema = FromJson<T>(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<T> FromJsonWithLoaderAsync<T>(
Func<T> loader,
SchemaType schemaType,
Expand Down
53 changes: 53 additions & 0 deletions src/NJsonSchema/JsonReferenceResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,59 @@ public virtual IJsonReference ResolveDocumentReference(object rootObject, string
return schema;
}

/// <summary>Gets the object from the given JSON path (synchronous version).
/// Only supports document-internal references (# and #/...). External file or URL
/// references will throw <see cref="NotSupportedException"/>.</summary>
/// <param name="rootObject">The root object.</param>
/// <param name="jsonPath">The JSON path.</param>
/// <param name="targetType">The target type to resolve.</param>
/// <param name="contractResolver">The contract resolver.</param>
/// <returns>The JSON Schema.</returns>
/// <exception cref="InvalidOperationException">Could not resolve the JSON path.</exception>
/// <exception cref="NotSupportedException">External file or URL references are not supported in synchronous mode.</exception>
public IJsonReference ResolveReference(object rootObject, string jsonPath, Type targetType, IContractResolver contractResolver)
{
return ResolveReference(rootObject, jsonPath, targetType, contractResolver, true);
}

/// <summary>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 <see cref="NotSupportedException"/>.</summary>
/// <param name="rootObject">The root object.</param>
/// <param name="jsonPath">The JSON path.</param>
/// <param name="targetType">The target type to resolve.</param>
/// <param name="contractResolver">The contract resolver.</param>
/// <returns>The JSON Schema.</returns>
/// <exception cref="InvalidOperationException">Could not resolve the JSON path.</exception>
/// <exception cref="NotSupportedException">External file or URL references are not supported in synchronous mode.</exception>
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.");
}
}

/// <summary>Resolves a file reference.</summary>
/// <param name="filePath">The file path.</param>
/// <param name="cancellationToken">The cancellation token</param>
Expand Down
35 changes: 35 additions & 0 deletions src/NJsonSchema/JsonSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,41 @@ public static Task<JsonSchema> FromJsonAsync(Stream stream, string? documentPath
return JsonSchemaSerialization.FromJsonAsync(stream, SerializationSchemaType, documentPath, referenceResolverFactory, ContractResolver.Value, cancellationToken);
}

/// <summary>Deserializes a JSON string to a <see cref="JsonSchema"/> (synchronous version).
/// Only supports document-internal references (# and #/...). External file or URL
/// references will throw <see cref="NotSupportedException"/>.</summary>
/// <param name="data">The JSON string.</param>
/// <returns>The JSON Schema.</returns>
public static JsonSchema FromJson(string data)
{
return FromJson(data, null);
}

/// <summary>Deserializes a JSON string to a <see cref="JsonSchema"/> (synchronous version).
/// Only supports document-internal references (# and #/...). External file or URL
/// references will throw <see cref="NotSupportedException"/>.</summary>
/// <param name="data">The JSON string.</param>
/// <param name="documentPath">The document path (URL or file path) for resolving relative document references.</param>
/// <returns>The JSON Schema.</returns>
public static JsonSchema FromJson(string data, string? documentPath)
{
var factory = JsonReferenceResolver.CreateJsonReferenceResolverFactory(new DefaultTypeNameGenerator());
return FromJson(data, documentPath, factory);
}

/// <summary>Deserializes a JSON string to a <see cref="JsonSchema" /> (synchronous version).
/// Only supports document-internal references (# and #/...). External file or URL
/// references will throw <see cref="NotSupportedException"/>.</summary>
/// <param name="data">The JSON string.</param>
/// <param name="documentPath">The document path (URL or file path) for resolving relative document references.</param>
/// <param name="referenceResolverFactory">The JSON reference resolver factory.</param>
/// <returns>The JSON Schema.</returns>
public static JsonSchema FromJson(string data, string? documentPath, Func<JsonSchema,
JsonReferenceResolver> referenceResolverFactory)
{
return JsonSchemaSerialization.FromJson(data, SerializationSchemaType, documentPath, referenceResolverFactory, ContractResolver.Value);
}

/// <summary>Creates a <see cref="JsonSchema" /> from a given type (using System.Text.Json rules).</summary>
/// <typeparam name="TType">The type to create the schema for.</typeparam>
/// <returns>The <see cref="JsonSchema" />.</returns>
Expand Down
71 changes: 71 additions & 0 deletions src/NJsonSchema/JsonSchemaReferenceUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,29 @@ public static async Task UpdateSchemaReferencesAsync(object rootObject, JsonRefe
await updater.VisitAsync(rootObject, cancellationToken).ConfigureAwait(false);
}

/// <summary>Updates all <see cref="IJsonReferenceBase.Reference"/> properties from the
/// available <see cref="IJsonReferenceBase.Reference"/> properties (synchronous version).
/// Only supports document-internal references (# and #/...). External file or URL
/// references will throw <see cref="NotSupportedException"/>.</summary>
/// <param name="rootObject">The root object.</param>
/// <param name="referenceResolver">The JSON document resolver.</param>
public static void UpdateSchemaReferences(object rootObject, JsonReferenceResolver referenceResolver) =>
UpdateSchemaReferences(rootObject, referenceResolver, new DefaultContractResolver());

/// <summary>Updates all <see cref="IJsonReferenceBase.Reference"/> properties from the
/// available <see cref="IJsonReferenceBase.Reference"/> properties (synchronous version).
/// Only supports document-internal references (# and #/...). External file or URL
/// references will throw <see cref="NotSupportedException"/>.</summary>
/// <param name="rootObject">The root object.</param>
/// <param name="referenceResolver">The JSON document resolver.</param>
/// <param name="contractResolver">The contract resolver.</param>
public static void UpdateSchemaReferences(object rootObject, JsonReferenceResolver referenceResolver,
IContractResolver contractResolver)
{
var updater = new JsonReferenceUpdaterSync(rootObject, referenceResolver, contractResolver);
updater.Visit(rootObject);
}

/// <summary>Updates the <see cref="IJsonReferenceBase.Reference" /> properties
/// from the available <see cref="IJsonReferenceBase.Reference" /> properties with inlining external references.</summary>
/// <param name="rootObject">The root object.</param>
Expand Down Expand Up @@ -121,6 +144,54 @@ protected override async Task<IJsonReference> 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;
Expand Down
Loading