diff --git a/src/NJsonSchema.CodeGeneration.CSharp/Models/EnumTemplateModel.cs b/src/NJsonSchema.CodeGeneration.CSharp/Models/EnumTemplateModel.cs index 5df04c9ce..d3ae5aae7 100644 --- a/src/NJsonSchema.CodeGeneration.CSharp/Models/EnumTemplateModel.cs +++ b/src/NJsonSchema.CodeGeneration.CSharp/Models/EnumTemplateModel.cs @@ -66,6 +66,8 @@ public IEnumerable Enums var value = _schema.Enumeration.ElementAt(i); if (value != null) { + var description = _schema.EnumerationDescriptions.Count > i ? + _schema.EnumerationDescriptions.ElementAt(i) : null; if (_schema.Type.IsInteger()) { var name = _schema.EnumerationNames.Count > i ? @@ -78,6 +80,7 @@ public IEnumerable Enums Name = _settings.EnumNameGenerator.Generate(i, name, value, _schema), OriginalName = name, Value = value.ToString()!, + Description = description, InternalValue = valueInt64.ToString(CultureInfo.InvariantCulture), InternalFlagValue = valueInt64.ToString(CultureInfo.InvariantCulture) }); @@ -89,6 +92,7 @@ public IEnumerable Enums Name = _settings.EnumNameGenerator.Generate(i, name, value, _schema), OriginalName = name, Value = value.ToString()!, + Description = description, InternalValue = value.ToString(), InternalFlagValue = (1 << i).ToString(CultureInfo.InvariantCulture) }); @@ -104,6 +108,7 @@ public IEnumerable Enums Name = _settings.EnumNameGenerator.Generate(i, name, value, _schema), OriginalName = name!, Value = value.ToString()!, + Description = description, InternalValue = i.ToString(CultureInfo.InvariantCulture), InternalFlagValue = (1 << i).ToString(CultureInfo.InvariantCulture) }); diff --git a/src/NJsonSchema.CodeGeneration.TypeScript/Models/EnumTemplateModel.cs b/src/NJsonSchema.CodeGeneration.TypeScript/Models/EnumTemplateModel.cs index e32478cb8..5134efa1d 100644 --- a/src/NJsonSchema.CodeGeneration.TypeScript/Models/EnumTemplateModel.cs +++ b/src/NJsonSchema.CodeGeneration.TypeScript/Models/EnumTemplateModel.cs @@ -57,12 +57,15 @@ public List Enums var name = _schema.EnumerationNames.Count > i ? _schema.EnumerationNames.ElementAt(i) : _schema.Type.IsInteger() ? "_" + value : value.ToString()!; + var description = _schema.EnumerationDescriptions.Count > i ? + _schema.EnumerationDescriptions.ElementAt(i) : null; entries.Add(new EnumerationItemModel { Name = _settings.EnumNameGenerator.Generate(i, name, value, _schema), OriginalName = name, Value = _schema.Type.IsInteger() ? value.ToString()! : "\"" + value + "\"", + Description = description, }); } } diff --git a/src/NJsonSchema.CodeGeneration/Models/EnumerationItemModel.cs b/src/NJsonSchema.CodeGeneration/Models/EnumerationItemModel.cs index 5407dda4f..475f83365 100644 --- a/src/NJsonSchema.CodeGeneration/Models/EnumerationItemModel.cs +++ b/src/NJsonSchema.CodeGeneration/Models/EnumerationItemModel.cs @@ -20,6 +20,9 @@ public class EnumerationItemModel /// Gets or sets the value. public required string Value { get; set; } + /// Gets or sets the description. + public string? Description { get; set; } + /// Gets or sets the internal value (e.g. the underlying/system value). public string? InternalValue { get; set; } diff --git a/src/NJsonSchema.Tests/Generation/EnumTests.cs b/src/NJsonSchema.Tests/Generation/EnumTests.cs index 54afbea55..b7dd9b66d 100644 --- a/src/NJsonSchema.Tests/Generation/EnumTests.cs +++ b/src/NJsonSchema.Tests/Generation/EnumTests.cs @@ -1,5 +1,7 @@ +using System.ComponentModel; using Newtonsoft.Json; using Newtonsoft.Json.Converters; +using NJsonSchema.CodeGeneration.Tests; using NJsonSchema.NewtonsoftJson.Generation; namespace NJsonSchema.Tests.Generation @@ -150,5 +152,110 @@ public async Task When_enum_does_not_have_FlagsAttribute_then_custom_property_is Assert.False(schema.IsFlagEnumerable); Assert.DoesNotContain("x-enumFlags", json); } + + public enum EnumWithDescriptions + { + [Description("First value description")] + FirstValue, + + [Description("Second value description")] + SecondValue, + + // No description for this one + ThirdValue + } + + [Fact] + public async Task When_enum_has_description_attributes_then_descriptions_are_included_in_schema() + { + // Arrange + + // Act + var schema = NewtonsoftJsonSchemaGenerator.FromType(new NewtonsoftJsonSchemaGeneratorSettings + { + SerializerSettings = + { + Converters = { new StringEnumConverter() } + } + }); + var json = schema.ToJson(); + + // Assert + Assert.Equal(3, schema.EnumerationDescriptions.Count); + Assert.Equal("First value description", schema.EnumerationDescriptions[0]); + Assert.Equal("Second value description", schema.EnumerationDescriptions[1]); + Assert.Null(schema.EnumerationDescriptions[2]); // No description for ThirdValue + + // Verify the JSON output contains the x-enumDescriptions property + await VerifyHelper.Verify(json); + } + + [Fact] + public async Task When_schema_has_x_enum_names_then_backward_compatibility_works() + { + // Arrange + var schema = new JsonSchema(); + schema.Type = JsonObjectType.String; + + schema.Enumeration.Clear(); + schema.Enumeration.Add("value1"); + schema.Enumeration.Add("value2"); + + schema.EnumerationNames.Clear(); + schema.EnumerationNames.Add("Name1"); + schema.EnumerationNames.Add("Name2"); + + // Act + var json = schema.ToJson(); + + // Assert + await VerifyHelper.Verify(json); + } + + [Fact] + public async Task When_schema_has_x_enum_varnames_then_backward_compatibility_works() + { + // Arrange + var schema = new JsonSchema(); + schema.Type = JsonObjectType.String; + + schema.Enumeration.Clear(); + schema.Enumeration.Add("value1"); + schema.Enumeration.Add("value2"); + + schema.EnumerationNames.Clear(); + schema.EnumerationNames.Add("VarName1"); + schema.EnumerationNames.Add("VarName2"); + + // Act + var json = schema.ToJson(); + + // Assert + await VerifyHelper.Verify(json); + } + + [Fact] + public async Task When_schema_has_x_enum_descriptions_then_backward_compatibility_works() + { + // Arrange + var schema = new JsonSchema(); + schema.Type = JsonObjectType.String; + + schema.Enumeration.Clear(); + schema.Enumeration.Add("value1"); + schema.Enumeration.Add("value2"); + schema.Enumeration.Add("value3"); + + schema.EnumerationDescriptions.Clear(); + schema.EnumerationDescriptions.Add("Desc1"); + schema.EnumerationDescriptions.Add("Desc2"); + schema.EnumerationDescriptions.Add(null); + + // Act + var json = schema.ToJson(); + + // Assert + await VerifyHelper.Verify(json); + } } -} \ No newline at end of file +} diff --git a/src/NJsonSchema.Tests/Generation/Snapshots/EnumTests.When_enum_has_description_attributes_then_descriptions_are_included_in_schema.verified.txt b/src/NJsonSchema.Tests/Generation/Snapshots/EnumTests.When_enum_has_description_attributes_then_descriptions_are_included_in_schema.verified.txt new file mode 100644 index 000000000..ad8a7ce82 --- /dev/null +++ b/src/NJsonSchema.Tests/Generation/Snapshots/EnumTests.When_enum_has_description_attributes_then_descriptions_are_included_in_schema.verified.txt @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "EnumWithDescriptions", + "type": "string", + "description": "", + "x-enum-names": [ + "FirstValue", + "SecondValue", + "ThirdValue" + ], + "x-enum-varnames": [ + "FirstValue", + "SecondValue", + "ThirdValue" + ], + "x-enumNames": [ + "FirstValue", + "SecondValue", + "ThirdValue" + ], + "x-enum-descriptions": [ + "First value description", + "Second value description", + null + ], + "x-enumDescriptions": [ + "First value description", + "Second value description", + null + ], + "enum": [ + "FirstValue", + "SecondValue", + "ThirdValue" + ] +} \ No newline at end of file diff --git a/src/NJsonSchema.Tests/Generation/Snapshots/EnumTests.When_schema_has_x_enum_descriptions_then_backward_compatibility_works.verified.txt b/src/NJsonSchema.Tests/Generation/Snapshots/EnumTests.When_schema_has_x_enum_descriptions_then_backward_compatibility_works.verified.txt new file mode 100644 index 000000000..f09996a14 --- /dev/null +++ b/src/NJsonSchema.Tests/Generation/Snapshots/EnumTests.When_schema_has_x_enum_descriptions_then_backward_compatibility_works.verified.txt @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "string", + "x-enum-descriptions": [ + "Desc1", + "Desc2", + null + ], + "x-enumDescriptions": [ + "Desc1", + "Desc2", + null + ], + "enum": [ + "value1", + "value2", + "value3" + ] +} \ No newline at end of file diff --git a/src/NJsonSchema.Tests/Generation/Snapshots/EnumTests.When_schema_has_x_enum_names_then_backward_compatibility_works.verified.txt b/src/NJsonSchema.Tests/Generation/Snapshots/EnumTests.When_schema_has_x_enum_names_then_backward_compatibility_works.verified.txt new file mode 100644 index 000000000..1625d5e1c --- /dev/null +++ b/src/NJsonSchema.Tests/Generation/Snapshots/EnumTests.When_schema_has_x_enum_names_then_backward_compatibility_works.verified.txt @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "string", + "x-enum-names": [ + "Name1", + "Name2" + ], + "x-enum-varnames": [ + "Name1", + "Name2" + ], + "x-enumNames": [ + "Name1", + "Name2" + ], + "enum": [ + "value1", + "value2" + ] +} \ No newline at end of file diff --git a/src/NJsonSchema.Tests/Generation/Snapshots/EnumTests.When_schema_has_x_enum_varnames_then_backward_compatibility_works.verified.txt b/src/NJsonSchema.Tests/Generation/Snapshots/EnumTests.When_schema_has_x_enum_varnames_then_backward_compatibility_works.verified.txt new file mode 100644 index 000000000..220cd800b --- /dev/null +++ b/src/NJsonSchema.Tests/Generation/Snapshots/EnumTests.When_schema_has_x_enum_varnames_then_backward_compatibility_works.verified.txt @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "string", + "x-enum-names": [ + "VarName1", + "VarName2" + ], + "x-enum-varnames": [ + "VarName1", + "VarName2" + ], + "x-enumNames": [ + "VarName1", + "VarName2" + ], + "enum": [ + "value1", + "value2" + ] +} \ No newline at end of file diff --git a/src/NJsonSchema/Generation/JsonSchemaGenerator.cs b/src/NJsonSchema/Generation/JsonSchemaGenerator.cs index 01bf1e82e..e01f2d038 100644 --- a/src/NJsonSchema/Generation/JsonSchemaGenerator.cs +++ b/src/NJsonSchema/Generation/JsonSchemaGenerator.cs @@ -14,6 +14,7 @@ using NJsonSchema.Generation.TypeMappers; using NJsonSchema.Infrastructure; using System.Collections; +using System.ComponentModel; using System.Globalization; using System.Linq; using System.Reflection; @@ -698,12 +699,23 @@ protected virtual void GenerateEnum(JsonSchema schema, JsonTypeDescription typeD schema.Type = typeDescription.Type; schema.Enumeration.Clear(); schema.EnumerationNames.Clear(); + schema.EnumerationDescriptions.Clear(); schema.IsFlagEnumerable = contextualType.IsAttributeDefined(true); Func? enumValueConverter = null; var underlyingType = Enum.GetUnderlyingType(contextualType.Type); foreach (var enumName in Enum.GetNames(contextualType.Type)) { + string? enumDescription = null; + var field = contextualType.Type.GetRuntimeField(enumName); + // Retrieve the Description attribute value, if present. + var descriptionAttribute = field?.GetCustomAttribute(); + + if (descriptionAttribute != null) + { + enumDescription = descriptionAttribute.Description; + } + if (typeDescription.Type == JsonObjectType.Integer) { var value = Convert.ChangeType(Enum.Parse(contextualType.Type, enumName), underlyingType, CultureInfo.InvariantCulture); @@ -712,7 +724,7 @@ protected virtual void GenerateEnum(JsonSchema schema, JsonTypeDescription typeD else { // EnumMember only checked if StringEnumConverter is used - var enumMemberAttribute = contextualType.Type.GetRuntimeField(enumName)?.GetCustomAttribute(); + var enumMemberAttribute = field?.GetCustomAttribute(); if (enumMemberAttribute != null && !string.IsNullOrEmpty(enumMemberAttribute.Value)) { schema.Enumeration.Add(enumMemberAttribute.Value); @@ -726,6 +738,7 @@ protected virtual void GenerateEnum(JsonSchema schema, JsonTypeDescription typeD } schema.EnumerationNames.Add(enumName); + schema.EnumerationDescriptions.Add(enumDescription); } if (typeDescription.Type == JsonObjectType.Integer && Settings.GenerateEnumMappingDescription) diff --git a/src/NJsonSchema/JsonSchema.Serialization.cs b/src/NJsonSchema/JsonSchema.Serialization.cs index 02fded269..aa5a62368 100644 --- a/src/NJsonSchema/JsonSchema.Serialization.cs +++ b/src/NJsonSchema/JsonSchema.Serialization.cs @@ -140,6 +140,10 @@ internal object? DiscriminatorRaw [JsonIgnore] public Collection EnumerationNames { get; set; } + /// Gets or sets the enumeration descriptions (optional, draft v5). + [JsonIgnore] + public Collection EnumerationDescriptions { get; set; } + /// Gets or sets a value indicating whether the maximum value is excluded. [JsonProperty("exclusiveMaximum", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] internal object? ExclusiveMaximumRaw @@ -373,6 +377,32 @@ internal IDictionary? DefinitionsRaw set => Definitions = value != null ? new ObservableDictionary(value!) : []; } + /// Gets or sets the enumeration names (optional, draft v5). + [JsonProperty("x-enum-names", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + internal Collection? EnumerationNamesDashedRaw + { + get => EnumerationNamesRaw; + set + { + if (EnumerationNamesRaw?.Count == 0 && value != null) { + EnumerationNamesRaw = value; + } + } + } + + /// Gets or sets the enumeration names (optional, draft v5). + [JsonProperty("x-enum-varnames", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + internal Collection? EnumerationVarnamesRaw + { + get => EnumerationNamesRaw; + set + { + if (EnumerationNamesRaw?.Count == 0 && value != null) { + EnumerationNamesRaw = value; + } + } + } + /// Gets or sets the enumeration names (optional, draft v5). [JsonProperty("x-enumNames", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] internal Collection? EnumerationNamesRaw @@ -381,6 +411,27 @@ internal Collection? EnumerationNamesRaw set => EnumerationNames = value != null ? new ObservableCollection(value) : []; } + /// Gets or sets the enumeration descriptions (optional, draft v5). + [JsonProperty("x-enum-descriptions", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + internal Collection? EnumerationDescriptionsDashedRaw + { + get => EnumerationDescriptionsRaw; + set + { + if (EnumerationDescriptionsRaw?.Count == 0 && value != null) { + EnumerationDescriptionsRaw = value; + } + } + } + + /// Gets or sets the enumeration descriptions (optional, draft v5). + [JsonProperty("x-enumDescriptions", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] + internal Collection? EnumerationDescriptionsRaw + { + get => EnumerationDescriptions != null && EnumerationDescriptions.Count > 0 ? EnumerationDescriptions : null; + set => EnumerationDescriptions = value != null ? new ObservableCollection(value) : []; + } + [JsonProperty("enum", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)] internal ICollection? EnumerationRaw { diff --git a/src/NJsonSchema/JsonSchema.cs b/src/NJsonSchema/JsonSchema.cs index 9e55548a4..98bf0282b 100644 --- a/src/NJsonSchema/JsonSchema.cs +++ b/src/NJsonSchema/JsonSchema.cs @@ -1014,6 +1014,7 @@ private static JsonObjectType ConvertStringToJsonObjectType(string? value) [MemberNotNull(nameof(_oneOf))] [MemberNotNull(nameof(Enumeration))] [MemberNotNull(nameof(EnumerationNames))] + [MemberNotNull(nameof(EnumerationDescriptions))] private void Initialize() #pragma warning disable CS8774 { @@ -1027,6 +1028,7 @@ private void Initialize() OneOf ??= new ObservableCollection(); Enumeration ??= []; EnumerationNames ??= []; + EnumerationDescriptions ??= []; } #pragma warning restore CS8774