Skip to content

Commit

Permalink
Recursive sample generation (#1561)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
leflings authored May 3, 2023
1 parent 2551c78 commit f1d244b
Show file tree
Hide file tree
Showing 3 changed files with 234 additions and 55 deletions.
164 changes: 164 additions & 0 deletions src/NJsonSchema.Tests/Generation/SampleJsonDataGeneratorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,170 @@ public async Task PropertyWithIntegerMinimumDefiniton()
Assert.Equal(1, testJson.SelectToken("body.numberContent.value").Value<int>());
}

[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<double>());
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<double>());
}

[Fact]
public async Task PropertyWithFloatMinimumDefinition()
Expand Down
122 changes: 67 additions & 55 deletions src/NJsonSchema/Generation/SampleJsonDataGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,83 +40,95 @@ public SampleJsonDataGenerator(SampleJsonDataGeneratorSettings settings)
/// <returns>The JSON token.</returns>
public JToken Generate(JsonSchema schema)
{
return Generate(schema, new HashSet<JsonSchema>());
var stack = new Stack<JsonSchema>();
stack.Push(schema);
return Generate(schema, stack);
}

private JToken Generate(JsonSchema schema, HashSet<JsonSchema> usedSchemas)
private JToken Generate(JsonSchema schema, Stack<JsonSchema> 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)
{
Expand Down
3 changes: 3 additions & 0 deletions src/NJsonSchema/Generation/SampleJsonDataGeneratorSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ public class SampleJsonDataGeneratorSettings
{
/// <summary>Gets or sets a value indicating whether to generate optional properties (default: true).</summary>
public bool GenerateOptionalProperties { get; set; } = true;

/// <summary>Gets or sets a value indicating the max level of recursion the generator is allowed to perform (default: 3)</summary>
public int MaxRecursionLevel { get; set; } = 3;
}
}

0 comments on commit f1d244b

Please sign in to comment.