Skip to content

Commit f1d244b

Browse files
authored
Recursive sample generation (#1561)
* Add tests that show generator doesnt handle definition re-use/recursion * Allow definition re-use/recursion in SampleJsonDataGenerator Adds a MaxRecursionLevel to SampleJsonDataGeneratorSettings * Add test of recursion level
1 parent 2551c78 commit f1d244b

File tree

3 files changed

+234
-55
lines changed

3 files changed

+234
-55
lines changed

src/NJsonSchema.Tests/Generation/SampleJsonDataGeneratorTests.cs

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,170 @@ public async Task PropertyWithIntegerMinimumDefiniton()
185185
Assert.Equal(1, testJson.SelectToken("body.numberContent.value").Value<int>());
186186
}
187187

188+
[Fact]
189+
public async Task SchemaWithRecursiveDefinition()
190+
{
191+
//// Arrange
192+
var data = @"{
193+
""$schema"": ""http://json-schema.org/draft-04/schema#"",
194+
""title"": ""test schema"",
195+
""type"": ""object"",
196+
""required"": [
197+
""body"", ""footer""
198+
],
199+
""properties"": {
200+
""body"": {
201+
""$ref"": ""#/definitions/body""
202+
},
203+
""footer"": {
204+
""$ref"": ""#/definitions/numberContent""
205+
}
206+
},
207+
""definitions"": {
208+
""body"": {
209+
""type"": ""object"",
210+
""additionalProperties"": false,
211+
""properties"": {
212+
""numberContent"": {
213+
""$ref"": ""#/definitions/numberContent""
214+
}
215+
}
216+
},
217+
""numberContent"": {
218+
""type"": ""object"",
219+
""additionalProperties"": false,
220+
""properties"": {
221+
""value"": {
222+
""type"": ""number"",
223+
""maximum"": 5.00001,
224+
""minimum"": 1.000012
225+
},
226+
""data"": {
227+
""$ref"": ""#/definitions/body""
228+
}
229+
}
230+
}
231+
}
232+
}";
233+
var generator = new SampleJsonDataGenerator();
234+
var schema = await JsonSchema.FromJsonAsync(data);
235+
//// Act
236+
var testJson = generator.Generate(schema);
237+
238+
//// Assert
239+
var footerToken = testJson.SelectToken("body.numberContent.data.numberContent.value");
240+
Assert.NotNull(footerToken);
241+
242+
var validationResult = schema.Validate(testJson);
243+
Assert.NotNull(validationResult);
244+
Assert.Equal(1.000012, testJson.SelectToken("footer.value").Value<double>());
245+
Assert.True(validationResult.Count > 0); // It is expected to fail validating the recursive properties (because of max recursion level)
246+
}
247+
248+
[Fact]
249+
public async Task GeneratorAdheresToMaxRecursionLevel()
250+
{
251+
//// Arrange
252+
var data = @"{
253+
""$schema"": ""http://json-schema.org/draft-04/schema#"",
254+
""title"": ""test schema"",
255+
""type"": ""object"",
256+
""required"": [
257+
""body"", ""footer""
258+
],
259+
""properties"": {
260+
""body"": {
261+
""$ref"": ""#/definitions/body""
262+
}
263+
},
264+
""definitions"": {
265+
""body"": {
266+
""type"": ""object"",
267+
""additionalProperties"": false,
268+
""properties"": {
269+
""text"": { ""type"": ""string"", ""enum"": [""my_string""] },
270+
""body"": {
271+
""$ref"": ""#/definitions/body""
272+
}
273+
}
274+
}
275+
}
276+
}";
277+
var generator = new SampleJsonDataGenerator(new SampleJsonDataGeneratorSettings() { MaxRecursionLevel = 2 });
278+
var schema = await JsonSchema.FromJsonAsync(data);
279+
//// Act
280+
var testJson = generator.Generate(schema);
281+
282+
//// Assert
283+
var secondBodyToken = testJson.SelectToken("body.body");
284+
Assert.NotNull(secondBodyToken);
285+
286+
var thirdBodyToken = testJson.SelectToken("body.body.body") as JValue;
287+
Assert.NotNull(thirdBodyToken);
288+
Assert.Equal(JTokenType.Null, thirdBodyToken.Type);
289+
290+
var validationResult = schema.Validate(testJson);
291+
Assert.NotNull(validationResult);
292+
Assert.True(validationResult.Count > 0); // It is expected to fail validating the recursive properties (because of max recursion level)
293+
}
294+
295+
[Fact]
296+
public async Task SchemaWithDefinitionUseMultipleTimes()
297+
{
298+
//// Arrange
299+
var data = @"{
300+
""$schema"": ""http://json-schema.org/draft-04/schema#"",
301+
""title"": ""test schema"",
302+
""type"": ""object"",
303+
""required"": [
304+
""body"", ""footer""
305+
],
306+
""properties"": {
307+
""body"": {
308+
""$ref"": ""#/definitions/body""
309+
},
310+
""footer"": {
311+
""$ref"": ""#/definitions/numberContent""
312+
}
313+
},
314+
""definitions"": {
315+
""body"": {
316+
""type"": ""object"",
317+
""additionalProperties"": false,
318+
""properties"": {
319+
""numberContent"": {
320+
""$ref"": ""#/definitions/numberContent""
321+
}
322+
}
323+
},
324+
""numberContent"": {
325+
""type"": ""object"",
326+
""additionalProperties"": false,
327+
""properties"": {
328+
""value"": {
329+
""type"": ""number"",
330+
""maximum"": 5.00001,
331+
""minimum"": 1.000012
332+
}
333+
}
334+
}
335+
}
336+
}";
337+
var generator = new SampleJsonDataGenerator();
338+
var schema = await JsonSchema.FromJsonAsync(data);
339+
340+
//// Act
341+
var testJson = generator.Generate(schema);
342+
343+
//// Assert
344+
var footerToken = testJson.SelectToken("footer.value");
345+
Assert.NotNull(footerToken);
346+
347+
var validationResult = schema.Validate(testJson);
348+
Assert.NotNull(validationResult);
349+
Assert.Equal(0, validationResult.Count);
350+
Assert.Equal(1.000012, testJson.SelectToken("body.numberContent.value").Value<double>());
351+
}
188352

189353
[Fact]
190354
public async Task PropertyWithFloatMinimumDefinition()

src/NJsonSchema/Generation/SampleJsonDataGenerator.cs

Lines changed: 67 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -40,83 +40,95 @@ public SampleJsonDataGenerator(SampleJsonDataGeneratorSettings settings)
4040
/// <returns>The JSON token.</returns>
4141
public JToken Generate(JsonSchema schema)
4242
{
43-
return Generate(schema, new HashSet<JsonSchema>());
43+
var stack = new Stack<JsonSchema>();
44+
stack.Push(schema);
45+
return Generate(schema, stack);
4446
}
4547

46-
private JToken Generate(JsonSchema schema, HashSet<JsonSchema> usedSchemas)
48+
private JToken Generate(JsonSchema schema, Stack<JsonSchema> schemaStack)
4749
{
4850
var property = schema as JsonSchemaProperty;
4951
schema = schema.ActualSchema;
50-
if (usedSchemas.Contains(schema))
52+
try
5153
{
52-
return null;
53-
}
54+
schemaStack.Push(schema);
55+
if (schemaStack.Count(s => s == schema) > _settings.MaxRecursionLevel)
56+
{
57+
return null;
58+
}
5459

55-
if (schema.Type.IsObject() || GetPropertiesToGenerate(schema.AllOf).Any())
56-
{
57-
usedSchemas.Add(schema);
60+
if (schema.Type.IsObject() || GetPropertiesToGenerate(schema.AllOf).Any())
61+
{
62+
var schemas = new[] { schema }.Concat(schema.AllOf.Select(x => x.ActualSchema));
63+
var properties = GetPropertiesToGenerate(schemas);
5864

59-
var schemas = new[] { schema }.Concat(schema.AllOf.Select(x => x.ActualSchema));
60-
var properties = GetPropertiesToGenerate(schemas);
65+
var obj = new JObject();
66+
foreach (var p in properties)
67+
{
68+
obj[p.Key] = Generate(p.Value, schemaStack);
69+
}
6170

62-
var obj = new JObject();
63-
foreach (var p in properties)
71+
return obj;
72+
}
73+
else if (schema.Default != null)
6474
{
65-
obj[p.Key] = Generate(p.Value, usedSchemas);
75+
return JToken.FromObject(schema.Default);
6676
}
67-
return obj;
68-
}
69-
else if (schema.Default != null)
70-
{
71-
return JToken.FromObject(schema.Default);
72-
}
73-
else if (schema.Type.IsArray())
74-
{
75-
if (schema.Item != null)
77+
else if (schema.Type.IsArray())
7678
{
77-
var array = new JArray();
78-
var item = Generate(schema.Item, usedSchemas);
79-
if (item != null)
79+
if (schema.Item != null)
80+
{
81+
var array = new JArray();
82+
83+
var item = Generate(schema.Item, schemaStack);
84+
if (item != null)
85+
{
86+
array.Add(item);
87+
}
88+
89+
return array;
90+
}
91+
else if (schema.Items.Count > 0)
8092
{
81-
array.Add(item);
93+
var array = new JArray();
94+
foreach (var item in schema.Items)
95+
{
96+
array.Add(Generate(item, schemaStack));
97+
}
98+
99+
return array;
82100
}
83-
return array;
84101
}
85-
else if (schema.Items.Count > 0)
102+
else
86103
{
87-
var array = new JArray();
88-
foreach (var item in schema.Items)
104+
if (schema.IsEnumeration)
105+
{
106+
return JToken.FromObject(schema.Enumeration.First());
107+
}
108+
else if (schema.Type.IsInteger())
89109
{
90-
array.Add(Generate(item, usedSchemas));
110+
return HandleIntegerType(schema);
111+
}
112+
else if (schema.Type.IsNumber())
113+
{
114+
return HandleNumberType(schema);
115+
}
116+
else if (schema.Type.IsString())
117+
{
118+
return HandleStringType(schema, property);
119+
}
120+
else if (schema.Type.IsBoolean())
121+
{
122+
return JToken.FromObject(false);
91123
}
92-
return array;
93124
}
125+
126+
return null;
94127
}
95-
else
128+
finally
96129
{
97-
if (schema.IsEnumeration)
98-
{
99-
return JToken.FromObject(schema.Enumeration.First());
100-
}
101-
else if (schema.Type.IsInteger())
102-
{
103-
return HandleIntegerType(schema);
104-
}
105-
else if (schema.Type.IsNumber())
106-
{
107-
return HandleNumberType(schema);
108-
}
109-
else if (schema.Type.IsString())
110-
{
111-
return HandleStringType(schema, property);
112-
}
113-
else if (schema.Type.IsBoolean())
114-
{
115-
return JToken.FromObject(false);
116-
}
130+
schemaStack.Pop();
117131
}
118-
119-
return null;
120132
}
121133
private JToken HandleNumberType(JsonSchema schema)
122134
{

src/NJsonSchema/Generation/SampleJsonDataGeneratorSettings.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,8 @@ public class SampleJsonDataGeneratorSettings
55
{
66
/// <summary>Gets or sets a value indicating whether to generate optional properties (default: true).</summary>
77
public bool GenerateOptionalProperties { get; set; } = true;
8+
9+
/// <summary>Gets or sets a value indicating the max level of recursion the generator is allowed to perform (default: 3)</summary>
10+
public int MaxRecursionLevel { get; set; } = 3;
811
}
912
}

0 commit comments

Comments
 (0)