diff --git a/src/NJsonSchema.NewtonsoftJson/Generation/NewtonsoftJsonReflectionService.cs b/src/NJsonSchema.NewtonsoftJson/Generation/NewtonsoftJsonReflectionService.cs index 3177cd40b..0fe6a603e 100644 --- a/src/NJsonSchema.NewtonsoftJson/Generation/NewtonsoftJsonReflectionService.cs +++ b/src/NJsonSchema.NewtonsoftJson/Generation/NewtonsoftJsonReflectionService.cs @@ -186,11 +186,15 @@ private void LoadPropertyOrField(JsonProperty jsonProperty, ContextualAccessorIn parentSchema.RequiredProperties.Add(propertyName); } - var isNullable = propertyTypeDescription.IsNullable && !hasRequiredAttribute && jsonProperty.Required is Required.Default or Required.AllowNull; + // The C# required keyword marks a property as required in the schema's required array + // but does not imply a non-nullable value. Only [Required] carries the semantic meaning + // of "non-null value required" and should suppress nullability and trigger MinLength = 1 + // on strings. + var isNullable = propertyTypeDescription.IsNullable && requiredAttribute == null && jsonProperty.Required is Required.Default or Required.AllowNull; var defaultValue = jsonProperty.DefaultValue; - schemaGenerator.AddProperty(parentSchema, accessorInfo, propertyTypeDescription, propertyName, requiredAttribute, hasRequiredAttribute, isNullable, defaultValue, schemaResolver); + schemaGenerator.AddProperty(parentSchema, accessorInfo, propertyTypeDescription, propertyName, requiredAttribute, requiredAttribute != null, isNullable, defaultValue, schemaResolver); } } diff --git a/src/NJsonSchema.Tests/Generation/AttributeGenerationTests.cs b/src/NJsonSchema.Tests/Generation/AttributeGenerationTests.cs index 9006db557..410fbd040 100644 --- a/src/NJsonSchema.Tests/Generation/AttributeGenerationTests.cs +++ b/src/NJsonSchema.Tests/Generation/AttributeGenerationTests.cs @@ -353,6 +353,28 @@ public void When_property_has_required_keyword_then_it_is_required_in_Newtonsoft Assert.Contains("Name", schema.RequiredProperties); Assert.DoesNotContain("Optional", schema.RequiredProperties); } + +#nullable enable + public class ClassWithRequiredNullableKeyword + { + public required string? Name { get; set; } + public string? Optional { get; set; } + } +#nullable restore + + [Fact] + public void When_property_has_required_keyword_and_nullable_type_then_it_is_required_and_nullable_in_Newtonsoft_schema() + { + // Act + var schema = NewtonsoftJsonSchemaGenerator.FromType(); + + // Assert: required keyword adds to required array + Assert.Contains("Name", schema.RequiredProperties); + Assert.DoesNotContain("Optional", schema.RequiredProperties); + // Assert: nullable type is preserved — required keyword alone must not suppress nullability + Assert.True(schema.Properties["Name"].IsNullable(SchemaType.JsonSchema)); + Assert.Null(schema.Properties["Name"].MinLength); + } #endif #if NET6_0_OR_GREATER diff --git a/src/NJsonSchema.Tests/Generation/SystemTextJson/SystemTextJsonTests.cs b/src/NJsonSchema.Tests/Generation/SystemTextJson/SystemTextJsonTests.cs index a1fa1dceb..3d02b3764 100644 --- a/src/NJsonSchema.Tests/Generation/SystemTextJson/SystemTextJsonTests.cs +++ b/src/NJsonSchema.Tests/Generation/SystemTextJson/SystemTextJsonTests.cs @@ -151,6 +151,28 @@ public void When_property_has_required_keyword_then_it_is_required_in_schema() Assert.DoesNotContain("Optional", schema.RequiredProperties); } +#nullable enable + public class ClassWithRequiredNullableKeyword + { + public required string? Name { get; set; } + public string? Optional { get; set; } + } +#nullable restore + + [Fact] + public void When_property_has_required_keyword_and_nullable_type_then_it_is_required_and_nullable_in_schema() + { + // Act + var schema = JsonSchema.FromType(); + + // Assert: required keyword adds to required array + Assert.Contains("Name", schema.RequiredProperties); + Assert.DoesNotContain("Optional", schema.RequiredProperties); + // Assert: nullable type is preserved — required keyword alone must not suppress nullability + Assert.True(schema.Properties["Name"].IsNullable(SchemaType.JsonSchema)); + Assert.Null(schema.Properties["Name"].MinLength); + } + public class ClassWithJsonRequired { [System.Text.Json.Serialization.JsonRequired] @@ -168,6 +190,30 @@ public void When_property_has_JsonRequired_then_it_is_required_in_schema() Assert.Contains("Name", schema.RequiredProperties); Assert.DoesNotContain("Optional", schema.RequiredProperties); } + +#nullable enable + public class ClassWithJsonRequiredNullable + { + [System.Text.Json.Serialization.JsonRequired] + public string? Name { get; set; } + public string? Optional { get; set; } + } +#nullable restore + + [Fact] + public void When_property_has_JsonRequired_and_nullable_type_then_it_is_required_and_nullable_in_schema() + { + // Act + var schema = JsonSchema.FromType(); + + // Assert: [JsonRequired] is a presence marker (like the C# required keyword) and matches + // System.Text.Json runtime semantics — the property must be present in the JSON, but the + // value may be null. Only [Required] (DataAnnotations) implies a non-null value. + Assert.Contains("Name", schema.RequiredProperties); + Assert.DoesNotContain("Optional", schema.RequiredProperties); + Assert.True(schema.Properties["Name"].IsNullable(SchemaType.JsonSchema)); + Assert.Null(schema.Properties["Name"].MinLength); + } #endif public class ClassWithPublicField diff --git a/src/NJsonSchema/Generation/SystemTextJsonReflectionService.cs b/src/NJsonSchema/Generation/SystemTextJsonReflectionService.cs index 190b2e689..2ac8a478e 100644 --- a/src/NJsonSchema/Generation/SystemTextJsonReflectionService.cs +++ b/src/NJsonSchema/Generation/SystemTextJsonReflectionService.cs @@ -104,10 +104,14 @@ public override void GenerateProperties(JsonSchema schema, ContextualType contex schema.RequiredProperties.Add(propertyName); } - var isNullable = propertyTypeDescription.IsNullable && !hasRequiredAttribute; + // Presence markers (C# required keyword, [JsonRequired], DataMember.IsRequired) only + // mark a property as required in the schema's required array. Only [Required] carries + // the semantic meaning of "non-null value required" and should suppress nullability + // and trigger MinLength = 1 on strings. + var isNullable = propertyTypeDescription.IsNullable && requiredAttribute == null; // TODO: Add default value - schemaGenerator.AddProperty(schema, accessorInfo, propertyTypeDescription, propertyName, requiredAttribute, hasRequiredAttribute, isNullable, null, schemaResolver); + schemaGenerator.AddProperty(schema, accessorInfo, propertyTypeDescription, propertyName, requiredAttribute, requiredAttribute != null, isNullable, null, schemaResolver); } } }