From 85b58f2015dbfbc7482aa48f33590f972be78118 Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Wed, 23 Apr 2025 10:17:18 +0200 Subject: [PATCH] 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 Co-authored-by: Andy Butland --- .../ConfigureUmbracoBackofficeJsonOptions.cs | 1 + .../ValidationProblemDetailsConverter.cs | 70 +++++++++++++++++++ .../BackOfficeSerializationTests.cs | 34 +++++++++ 3 files changed, 105 insertions(+) create mode 100644 src/Umbraco.Cms.Api.Management/Serialization/ValidationProblemDetailsConverter.cs diff --git a/src/Umbraco.Cms.Api.Management/Serialization/ConfigureUmbracoBackofficeJsonOptions.cs b/src/Umbraco.Cms.Api.Management/Serialization/ConfigureUmbracoBackofficeJsonOptions.cs index d66d47cf16..436bd72fa3 100644 --- a/src/Umbraco.Cms.Api.Management/Serialization/ConfigureUmbracoBackofficeJsonOptions.cs +++ b/src/Umbraco.Cms.Api.Management/Serialization/ConfigureUmbracoBackofficeJsonOptions.cs @@ -33,6 +33,7 @@ public class ConfigureUmbracoBackofficeJsonOptions : IConfigureNamedOptions +/// Custom JSON converter registered via to serialize to JSON. +/// +public sealed class ValidationProblemDetailsConverter : JsonConverter +{ + /// + public override ValidationProblemDetails? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => null; + + /// + 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 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(); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Serialization/BackOfficeSerializationTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Serialization/BackOfficeSerializationTests.cs index aa82a37c6d..2200eaccad 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Serialization/BackOfficeSerializationTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Serialization/BackOfficeSerializationTests.cs @@ -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) { var root = new NestedJsonTestValue { Level = 1 }; @@ -77,4 +88,27 @@ public class BackOfficeSerializationTests 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"], + } + }; + } }