Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recursive sample generation #1561

Merged
merged 3 commits into from
May 3, 2023
Merged
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
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't this kill performance? capturing lambda and used inside recursion, having O(n) characteristics?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be very surprised if people use this functionality in performance sensitive scenarios. It's easy to rewrite this without the use of a capturing lambda if you deem this to be a big performance issue

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah true, this is only for sample data indeed. Anything called by NSwag in normal generation process usually is quite performance sensitive.

{
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;
}
}