Skip to content

Commit c28ff3f

Browse files
authored
DGS-18985 Fix JSON Schema ref handling (confluentinc#2339)
* DGS-18985 Fix JSON Schema ref handling * Minor cleanup * Minor cleanup
1 parent 69e9b4c commit c28ff3f

File tree

2 files changed

+156
-17
lines changed

2 files changed

+156
-17
lines changed

src/Confluent.SchemaRegistry.Serdes.Json/JsonSchemaResolver.cs

Lines changed: 75 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@
1616

1717
using System;
1818
using System.Collections.Generic;
19+
using System.IO;
20+
using System.Text.RegularExpressions;
21+
using System.Threading;
1922
using System.Threading.Tasks;
2023
using NJsonSchema;
24+
using NJsonSchema.References;
2125
using Newtonsoft.Json.Linq;
2226
#if NET8_0_OR_GREATER
2327
using NewtonsoftJsonSchemaGeneratorSettings = NJsonSchema.NewtonsoftJson.Generation.NewtonsoftJsonSchemaGeneratorSettings;
@@ -81,20 +85,17 @@ public async Task<JsonSchema> GetResolvedSchema(){
8185
return resolvedJsonSchema;
8286
}
8387

84-
private async Task CreateSchemaDictUtil(Schema root)
88+
private async Task CreateSchemaDictUtil(Schema root, string referenceName = null)
8589
{
86-
string rootStr = root.SchemaString;
87-
JObject schema = JObject.Parse(rootStr);
88-
string schemaId = (string)schema["$id"];
89-
if (schemaId != null && !dictSchemaNameToSchema.ContainsKey(schemaId))
90-
this.dictSchemaNameToSchema.Add(schemaId, root);
90+
if (referenceName != null && !dictSchemaNameToSchema.ContainsKey(referenceName))
91+
this.dictSchemaNameToSchema.Add(referenceName, root);
9192

9293
if (root.References != null)
9394
{
9495
foreach (var reference in root.References)
9596
{
9697
Schema refSchemaRes = await schemaRegistryClient.GetRegisteredSchemaAsync(reference.Subject, reference.Version, false);
97-
await CreateSchemaDictUtil(refSchemaRes);
98+
await CreateSchemaDictUtil(refSchemaRes, reference.Name);
9899
}
99100
}
100101
}
@@ -118,21 +119,78 @@ private async Task<JsonSchema> GetSchemaUtil(Schema root)
118119
new NJsonSchema.Generation.JsonSchemaResolver(rootObject, this.jsonSchemaGeneratorSettings ??
119120
new NewtonsoftJsonSchemaGeneratorSettings());
120121

121-
JsonReferenceResolver referenceResolver =
122-
new JsonReferenceResolver(schemaResolver);
123-
foreach (var reference in refers)
124-
{
125-
JsonSchema jschema =
126-
dictSchemaNameToJsonSchema[reference.Name];
127-
referenceResolver.AddDocumentReference(reference.Name, jschema);
128-
}
129-
return referenceResolver;
122+
return new CustomJsonReferenceResolver(schemaResolver, rootObject, dictSchemaNameToJsonSchema);
130123
};
131124

132125
string rootStr = root.SchemaString;
133126
JObject schema = JObject.Parse(rootStr);
134-
string schemaId = (string)schema["$id"];
127+
string schemaId = (string)schema["$id"] ?? "";
135128
return await JsonSchema.FromJsonAsync(rootStr, schemaId, factory);
136129
}
130+
131+
private class CustomJsonReferenceResolver : JsonReferenceResolver
132+
{
133+
private JsonSchema rootObject;
134+
private Dictionary<string, JsonSchema> refs;
135+
136+
public CustomJsonReferenceResolver(JsonSchemaAppender schemaAppender,
137+
JsonSchema rootObject, Dictionary<string, JsonSchema> refs)
138+
: base( schemaAppender)
139+
{
140+
this.rootObject = rootObject;
141+
this.refs = refs;
142+
}
143+
144+
public override string ResolveFilePath(string documentPath, string jsonPath)
145+
{
146+
// override the default behavior to not prepend the documentPath
147+
var arr = Regex.Split(jsonPath, @"(?=#)");
148+
return arr[0];
149+
}
150+
151+
public override async Task<IJsonReference> ResolveFileReferenceAsync(string filePath, CancellationToken cancellationToken = default)
152+
{
153+
JsonSchema schema;
154+
if (refs.TryGetValue(filePath, out schema))
155+
{
156+
return schema;
157+
}
158+
159+
// remove the documentPath and look for the reference
160+
var fileName = Path.GetFileName(filePath);
161+
if (refs.TryGetValue(fileName, out schema))
162+
{
163+
return schema;
164+
}
165+
166+
return await base.ResolveFileReferenceAsync(filePath, cancellationToken);
167+
}
168+
169+
public override async Task<IJsonReference> ResolveUrlReferenceAsync(string url, CancellationToken cancellationToken = default)
170+
{
171+
JsonSchema schema;
172+
if (refs.TryGetValue(url, out schema))
173+
{
174+
return schema;
175+
}
176+
177+
var documentPathProvider = rootObject as IDocumentPathProvider;
178+
var documentPath = documentPathProvider?.DocumentPath;
179+
if (documentPath != null)
180+
{
181+
var documentUri = new Uri(documentPath);
182+
var uri = new Uri(url);
183+
var relativeUrl = documentUri.MakeRelativeUri(uri);
184+
185+
// remove the documentPath and look for the reference
186+
if (refs.TryGetValue(relativeUrl.ToString(), out schema))
187+
{
188+
return schema;
189+
}
190+
}
191+
192+
return await base.ResolveUrlReferenceAsync(url, cancellationToken);
193+
}
194+
}
137195
}
138196
}

test/Confluent.SchemaRegistry.Serdes.UnitTests/JsonSerializeDeserialize.cs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,36 @@ public class JsonSerializeDeserializeTests : BaseSerializeDeserializeTests
7171
}
7272
}
7373
}
74+
";
75+
public string schema1NoId = @"
76+
{
77+
""$schema"": ""http://json-schema.org/draft-07/schema#"",
78+
""title"": ""Schema1"",
79+
""type"": ""object"",
80+
""properties"": {
81+
""field1"": {
82+
""type"": ""string""
83+
},
84+
""field2"": {
85+
""type"": ""integer""
86+
},
87+
""field3"": {
88+
""$ref"": ""http://schema2.json#/definitions/field""
89+
}
90+
}
91+
}
92+
";
93+
public string schema2NoId = @"
94+
{
95+
""$schema"": ""http://json-schema.org/draft-07/schema#"",
96+
""title"": ""Schema2"",
97+
""type"": ""object"",
98+
""definitions"": {
99+
""field"": {
100+
""type"": ""boolean""
101+
}
102+
}
103+
}
74104
";
75105
public class Schema1
76106
{
@@ -262,6 +292,57 @@ public async Task WithJsonSchemaExternalReferencesAsync()
262292
Assert.Equal(v.Field3, actual.Field3);
263293
}
264294

295+
[Fact]
296+
public async Task WithJsonSchemaExternalReferencesNoIdAsync()
297+
{
298+
var subject1 = $"{testTopic}-Schema1";
299+
var subject2 = $"{testTopic}-Schema2";
300+
301+
var registeredSchema2 = new RegisteredSchema(subject2, 1, 1, schema2NoId, SchemaType.Json, null);
302+
store[schema2NoId] = 1;
303+
subjectStore[subject2] = new List<RegisteredSchema> { registeredSchema2 };
304+
305+
var refs = new List<SchemaReference> { new SchemaReference("http://schema2.json", subject2, 1) };
306+
var registeredSchema1 = new RegisteredSchema(subject1, 1, 2, schema1NoId, SchemaType.Json, refs);
307+
store[schema1NoId] = 2;
308+
subjectStore[subject1] = new List<RegisteredSchema> { registeredSchema1 };
309+
310+
var jsonSerializerConfig = new JsonSerializerConfig
311+
{
312+
UseLatestVersion = true,
313+
AutoRegisterSchemas = false,
314+
SubjectNameStrategy = SubjectNameStrategy.TopicRecord
315+
};
316+
317+
var jsonSchemaGeneratorSettings = new NewtonsoftJsonSchemaGeneratorSettings
318+
{
319+
SerializerSettings = new JsonSerializerSettings
320+
{
321+
ContractResolver = new DefaultContractResolver
322+
{
323+
NamingStrategy = new CamelCaseNamingStrategy()
324+
}
325+
}
326+
};
327+
328+
var jsonSerializer = new JsonSerializer<Schema1>(schemaRegistryClient, registeredSchema1,
329+
jsonSerializerConfig, jsonSchemaGeneratorSettings);
330+
var jsonDeserializer = new JsonDeserializer<Schema1>(schemaRegistryClient, registeredSchema1);
331+
var v = new Schema1
332+
{
333+
Field1 = "Hello",
334+
Field2 = 123,
335+
Field3 = true
336+
};
337+
string expectedJson = "{\"field1\":\"Hello\",\"field2\":123,\"field3\":true}";
338+
var bytes = await jsonSerializer.SerializeAsync(v, new SerializationContext(MessageComponentType.Value, testTopic));
339+
Assert.NotNull(bytes);
340+
Assert.Equal(expectedJson, Encoding.UTF8.GetString(bytes.AsSpan().Slice(5)));
341+
342+
var actual = await jsonDeserializer.DeserializeAsync(bytes, false, new SerializationContext(MessageComponentType.Value, testTopic));
343+
Assert.Equal(v.Field3, actual.Field3);
344+
}
345+
265346
#if NET8_0_OR_GREATER
266347
[Theory]
267348
[InlineData("CamelCaseString", EnumType.EnumValue, "{\"Value\":\"enumValue\"}")]

0 commit comments

Comments
 (0)