From f1d244be8c6bc7fc2052e9ddb64d2489192c61a3 Mon Sep 17 00:00:00 2001 From: Flemming Madsen Date: Wed, 3 May 2023 22:17:22 +0200 Subject: [PATCH] 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 --- .../SampleJsonDataGeneratorTests.cs | 164 ++++++++++++++++++ .../Generation/SampleJsonDataGenerator.cs | 122 +++++++------ .../SampleJsonDataGeneratorSettings.cs | 3 + 3 files changed, 234 insertions(+), 55 deletions(-) diff --git a/src/NJsonSchema.Tests/Generation/SampleJsonDataGeneratorTests.cs b/src/NJsonSchema.Tests/Generation/SampleJsonDataGeneratorTests.cs index 41b9a0fb8..79f44a3bf 100644 --- a/src/NJsonSchema.Tests/Generation/SampleJsonDataGeneratorTests.cs +++ b/src/NJsonSchema.Tests/Generation/SampleJsonDataGeneratorTests.cs @@ -185,6 +185,170 @@ public async Task PropertyWithIntegerMinimumDefiniton() Assert.Equal(1, testJson.SelectToken("body.numberContent.value").Value()); } + [Fact] + public async Task SchemaWithRecursiveDefinition() + { + //// Arrange + var data = @"{ + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""title"": ""test schema"", + ""type"": ""object"", + ""required"": [ + ""body"", ""footer"" + ], + ""properties"": { + ""body"": { + ""$ref"": ""#/definitions/body"" + }, + ""footer"": { + ""$ref"": ""#/definitions/numberContent"" + } + }, + ""definitions"": { + ""body"": { + ""type"": ""object"", + ""additionalProperties"": false, + ""properties"": { + ""numberContent"": { + ""$ref"": ""#/definitions/numberContent"" + } + } + }, + ""numberContent"": { + ""type"": ""object"", + ""additionalProperties"": false, + ""properties"": { + ""value"": { + ""type"": ""number"", + ""maximum"": 5.00001, + ""minimum"": 1.000012 + }, + ""data"": { + ""$ref"": ""#/definitions/body"" + } + } + } + } + }"; + var generator = new SampleJsonDataGenerator(); + var schema = await JsonSchema.FromJsonAsync(data); + //// Act + var testJson = generator.Generate(schema); + + //// Assert + var footerToken = testJson.SelectToken("body.numberContent.data.numberContent.value"); + Assert.NotNull(footerToken); + + var validationResult = schema.Validate(testJson); + Assert.NotNull(validationResult); + Assert.Equal(1.000012, testJson.SelectToken("footer.value").Value()); + Assert.True(validationResult.Count > 0); // It is expected to fail validating the recursive properties (because of max recursion level) + } + + [Fact] + public async Task GeneratorAdheresToMaxRecursionLevel() + { + //// Arrange + var data = @"{ + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""title"": ""test schema"", + ""type"": ""object"", + ""required"": [ + ""body"", ""footer"" + ], + ""properties"": { + ""body"": { + ""$ref"": ""#/definitions/body"" + } + }, + ""definitions"": { + ""body"": { + ""type"": ""object"", + ""additionalProperties"": false, + ""properties"": { + ""text"": { ""type"": ""string"", ""enum"": [""my_string""] }, + ""body"": { + ""$ref"": ""#/definitions/body"" + } + } + } + } + }"; + var generator = new SampleJsonDataGenerator(new SampleJsonDataGeneratorSettings() { MaxRecursionLevel = 2 }); + var schema = await JsonSchema.FromJsonAsync(data); + //// Act + var testJson = generator.Generate(schema); + + //// Assert + var secondBodyToken = testJson.SelectToken("body.body"); + Assert.NotNull(secondBodyToken); + + var thirdBodyToken = testJson.SelectToken("body.body.body") as JValue; + Assert.NotNull(thirdBodyToken); + Assert.Equal(JTokenType.Null, thirdBodyToken.Type); + + var validationResult = schema.Validate(testJson); + Assert.NotNull(validationResult); + Assert.True(validationResult.Count > 0); // It is expected to fail validating the recursive properties (because of max recursion level) + } + + [Fact] + public async Task SchemaWithDefinitionUseMultipleTimes() + { + //// Arrange + var data = @"{ + ""$schema"": ""http://json-schema.org/draft-04/schema#"", + ""title"": ""test schema"", + ""type"": ""object"", + ""required"": [ + ""body"", ""footer"" + ], + ""properties"": { + ""body"": { + ""$ref"": ""#/definitions/body"" + }, + ""footer"": { + ""$ref"": ""#/definitions/numberContent"" + } + }, + ""definitions"": { + ""body"": { + ""type"": ""object"", + ""additionalProperties"": false, + ""properties"": { + ""numberContent"": { + ""$ref"": ""#/definitions/numberContent"" + } + } + }, + ""numberContent"": { + ""type"": ""object"", + ""additionalProperties"": false, + ""properties"": { + ""value"": { + ""type"": ""number"", + ""maximum"": 5.00001, + ""minimum"": 1.000012 + } + } + } + } + }"; + var generator = new SampleJsonDataGenerator(); + var schema = await JsonSchema.FromJsonAsync(data); + + //// Act + var testJson = generator.Generate(schema); + + //// Assert + var footerToken = testJson.SelectToken("footer.value"); + Assert.NotNull(footerToken); + + var validationResult = schema.Validate(testJson); + Assert.NotNull(validationResult); + Assert.Equal(0, validationResult.Count); + Assert.Equal(1.000012, testJson.SelectToken("body.numberContent.value").Value()); + } [Fact] public async Task PropertyWithFloatMinimumDefinition() diff --git a/src/NJsonSchema/Generation/SampleJsonDataGenerator.cs b/src/NJsonSchema/Generation/SampleJsonDataGenerator.cs index 78caf0c54..9b42ec9f6 100644 --- a/src/NJsonSchema/Generation/SampleJsonDataGenerator.cs +++ b/src/NJsonSchema/Generation/SampleJsonDataGenerator.cs @@ -40,83 +40,95 @@ public SampleJsonDataGenerator(SampleJsonDataGeneratorSettings settings) /// The JSON token. public JToken Generate(JsonSchema schema) { - return Generate(schema, new HashSet()); + var stack = new Stack(); + stack.Push(schema); + return Generate(schema, stack); } - private JToken Generate(JsonSchema schema, HashSet usedSchemas) + private JToken Generate(JsonSchema schema, Stack schemaStack) { var property = schema as JsonSchemaProperty; schema = schema.ActualSchema; - if (usedSchemas.Contains(schema)) + try { - return null; - } + schemaStack.Push(schema); + if (schemaStack.Count(s => s == schema) > _settings.MaxRecursionLevel) + { + return null; + } - if (schema.Type.IsObject() || GetPropertiesToGenerate(schema.AllOf).Any()) - { - usedSchemas.Add(schema); + if (schema.Type.IsObject() || GetPropertiesToGenerate(schema.AllOf).Any()) + { + var schemas = new[] { schema }.Concat(schema.AllOf.Select(x => x.ActualSchema)); + var properties = GetPropertiesToGenerate(schemas); - var schemas = new[] { schema }.Concat(schema.AllOf.Select(x => x.ActualSchema)); - var properties = GetPropertiesToGenerate(schemas); + var obj = new JObject(); + foreach (var p in properties) + { + obj[p.Key] = Generate(p.Value, schemaStack); + } - var obj = new JObject(); - foreach (var p in properties) + return obj; + } + else if (schema.Default != null) { - obj[p.Key] = Generate(p.Value, usedSchemas); + return JToken.FromObject(schema.Default); } - return obj; - } - else if (schema.Default != null) - { - return JToken.FromObject(schema.Default); - } - else if (schema.Type.IsArray()) - { - if (schema.Item != null) + else if (schema.Type.IsArray()) { - var array = new JArray(); - var item = Generate(schema.Item, usedSchemas); - if (item != null) + if (schema.Item != null) + { + var array = new JArray(); + + var item = Generate(schema.Item, schemaStack); + if (item != null) + { + array.Add(item); + } + + return array; + } + else if (schema.Items.Count > 0) { - array.Add(item); + var array = new JArray(); + foreach (var item in schema.Items) + { + array.Add(Generate(item, schemaStack)); + } + + return array; } - return array; } - else if (schema.Items.Count > 0) + else { - var array = new JArray(); - foreach (var item in schema.Items) + if (schema.IsEnumeration) + { + return JToken.FromObject(schema.Enumeration.First()); + } + else if (schema.Type.IsInteger()) { - array.Add(Generate(item, usedSchemas)); + return HandleIntegerType(schema); + } + else if (schema.Type.IsNumber()) + { + return HandleNumberType(schema); + } + else if (schema.Type.IsString()) + { + return HandleStringType(schema, property); + } + else if (schema.Type.IsBoolean()) + { + return JToken.FromObject(false); } - return array; } + + return null; } - else + finally { - if (schema.IsEnumeration) - { - return JToken.FromObject(schema.Enumeration.First()); - } - else if (schema.Type.IsInteger()) - { - return HandleIntegerType(schema); - } - else if (schema.Type.IsNumber()) - { - return HandleNumberType(schema); - } - else if (schema.Type.IsString()) - { - return HandleStringType(schema, property); - } - else if (schema.Type.IsBoolean()) - { - return JToken.FromObject(false); - } + schemaStack.Pop(); } - - return null; } private JToken HandleNumberType(JsonSchema schema) { diff --git a/src/NJsonSchema/Generation/SampleJsonDataGeneratorSettings.cs b/src/NJsonSchema/Generation/SampleJsonDataGeneratorSettings.cs index 055e6ac7a..0dee8e309 100644 --- a/src/NJsonSchema/Generation/SampleJsonDataGeneratorSettings.cs +++ b/src/NJsonSchema/Generation/SampleJsonDataGeneratorSettings.cs @@ -5,5 +5,8 @@ public class SampleJsonDataGeneratorSettings { /// Gets or sets a value indicating whether to generate optional properties (default: true). public bool GenerateOptionalProperties { get; set; } = true; + + /// Gets or sets a value indicating the max level of recursion the generator is allowed to perform (default: 3) + public int MaxRecursionLevel { get; set; } = 3; } }