Custom serialization for ValidationProblemDetails to allign paths in custom and mvc error messages (#19111)

* Rowrked #18771 with a more brute force solution

* Fixes class name. Adds unit test verifying behaviour.

* Corrected test name.

---------

Co-authored-by: Kenn Jacobsen <kja@umbraco.dk>
Co-authored-by: Andy Butland <abutland73@gmail.com>
This commit is contained in:
Sven Geusens
2025-04-23 10:17:18 +02:00
committed by GitHub
parent d568e981ab
commit 85b58f2015
3 changed files with 105 additions and 0 deletions

View File

@@ -33,6 +33,7 @@ public class ConfigureUmbracoBackofficeJsonOptions : IConfigureNamedOptions<Json
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
options.JsonSerializerOptions.Converters.Add(new JsonUdiConverter()); options.JsonSerializerOptions.Converters.Add(new JsonUdiConverter());
options.JsonSerializerOptions.Converters.Add(new JsonUdiRangeConverter()); options.JsonSerializerOptions.Converters.Add(new JsonUdiRangeConverter());
options.JsonSerializerOptions.Converters.Add(new ValidationProblemDetailsConverter());
options.JsonSerializerOptions.Converters.Add(new JsonObjectConverter()); options.JsonSerializerOptions.Converters.Add(new JsonObjectConverter());
options.JsonSerializerOptions.TypeInfoResolver = _umbracoJsonTypeInfoResolver; options.JsonSerializerOptions.TypeInfoResolver = _umbracoJsonTypeInfoResolver;

View File

@@ -0,0 +1,70 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Serialization;
/// <summary>
/// Custom JSON converter registered via <see cref="ConfigureUmbracoBackofficeJsonOptions"/> to serialize <see cref="ValidationProblemDetails" /> to JSON.
/// </summary>
public sealed class ValidationProblemDetailsConverter : JsonConverter<ValidationProblemDetails>
{
/// <inheritdoc />
public override ValidationProblemDetails? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> null;
/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, ValidationProblemDetails value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteString("type", value.Type);
writer.WriteString("title", value.Title);
if (value.Status.HasValue)
{
writer.WriteNumber("status", value.Status.Value);
}
if (value.Detail is not null)
{
writer.WriteString("detail", value.Detail);
}
if (value.Instance is not null)
{
writer.WriteString("instance", value.Instance);
}
if (value.Errors.Any())
{
writer.WritePropertyName("errors");
writer.WriteStartArray();
foreach (KeyValuePair<string, string[]> error in value.Errors)
{
writer.WriteStartObject();
writer.WritePropertyName($"$.{string.Join(".", error.Key.Split(Constants.CharArrays.Period).Select(s => s.ToFirstLowerInvariant()))}");
writer.WriteStartArray();
foreach (var errorDetails in error.Value)
{
writer.WriteStringValue(errorDetails);
}
writer.WriteEndArray();
writer.WriteEndObject();
}
writer.WriteEndArray();
}
if (value.Extensions.TryGetValue("traceId", out var traceId) && traceId is string traceIdValue)
{
writer.WriteString("traceId", traceIdValue);
}
writer.WriteEndObject();
}
}

View File

@@ -52,6 +52,17 @@ public class BackOfficeSerializationTests
} }
} }
[Test]
public void Will_Serialize_ValidationProblemDetails_To_Casing_Aligned_With_Mvc()
{
var objectToSerialize = new TestValueWithValidationProblemDetail();
var json = JsonSerializer.Serialize(objectToSerialize, jsonOptions.JsonSerializerOptions);
var expectedJson = "{\"problemDetails\":{\"type\":\"Test type\",\"title\":\"Test title\",\"status\":400,\"detail\":\"Test detail\",\"instance\":\"Test instance\",\"errors\":[{\"$.testError1\":[\"Test error 1a\",\"Test error 1b\"]},{\"$.testError2\":[\"Test error 2a\"]},{\"$.testError3.testError3a\":[\"Test error 3b\"]}],\"traceId\":\"traceValue\"}}";
Assert.AreEqual(expectedJson, json);
}
private static NestedJsonTestValue CreateNestedObject(int levels) private static NestedJsonTestValue CreateNestedObject(int levels)
{ {
var root = new NestedJsonTestValue { Level = 1 }; var root = new NestedJsonTestValue { Level = 1 };
@@ -77,4 +88,27 @@ public class BackOfficeSerializationTests
public NestedJsonTestValue? Inner { get; set; } public NestedJsonTestValue? Inner { get; set; }
} }
private class TestValueWithValidationProblemDetail
{
public ValidationProblemDetails ProblemDetails { get; set; } = new()
{
Title = "Test title",
Detail = "Test detail",
Status = 400,
Type = "Test type",
Instance = "Test instance",
Extensions =
{
["traceId"] = "traceValue",
["someOtherExtension"] = "someOtherExtensionValue",
},
Errors =
{
["TestError1"] = ["Test error 1a", "Test error 1b"],
["TestError2"] = ["Test error 2a"],
["TestError3.TestError3a"] = ["Test error 3b"],
}
};
}
} }