using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; namespace GB5Shared.JsonConverter { // Legacy grid-style clients occasionally wrap a single-record save payload in a // one-element JSON array (a holdover from batch-save grid components) instead of // posting the bare object [FromBody] expects. Apply via // [JsonConverter(typeof(SingleOrArrayJsonConverter))] on the DTO class: // a plain object deserializes normally; a JSON array deserializes its first // element (an empty array yields default(T)). public class SingleOrArrayJsonConverter : JsonConverter { public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var innerOptions = WithoutSelf(options); if (reader.TokenType == JsonTokenType.StartArray) { var list = JsonSerializer.Deserialize>(ref reader, innerOptions); return (list != null && list.Count > 0) ? list[0] : default; } return JsonSerializer.Deserialize(ref reader, innerOptions); } public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { JsonSerializer.Serialize(writer, value, WithoutSelf(options)); } // Re-entering JsonSerializer for the same type T would otherwise pick this // same converter back up (it's registered on T via the class-level attribute) // and recurse forever, so strip it from a cloned options instance first. private static JsonSerializerOptions WithoutSelf(JsonSerializerOptions options) { var clone = new JsonSerializerOptions(options); for (int i = clone.Converters.Count - 1; i >= 0; i--) { if (clone.Converters[i] is SingleOrArrayJsonConverter) clone.Converters.RemoveAt(i); } return clone; } } }