feat(commons): native-typed JSON for List values; Decode reads both forms
This commit is contained in:
@@ -25,11 +25,10 @@ public static class AttributeValueCodec
|
|||||||
case string s: return s; // already canonical
|
case string s: return s; // already canonical
|
||||||
case IFormattable f: return f.ToString(null, CultureInfo.InvariantCulture);
|
case IFormattable f: return f.ToString(null, CultureInfo.InvariantCulture);
|
||||||
case IEnumerable e:
|
case IEnumerable e:
|
||||||
var items = e.Cast<object?>()
|
// Native-typed JSON: serialize the runtime collection so System.Text.Json emits
|
||||||
.Select(x => x is IFormattable xf
|
// numbers/bools unquoted, strings quoted, and DateTime as ISO-8601. Boxed as object
|
||||||
? xf.ToString(null, CultureInfo.InvariantCulture)
|
// so STJ uses the runtime element type. STJ numbers/dates are culture-invariant.
|
||||||
: x?.ToString());
|
return JsonSerializer.Serialize<object>(e, JsonOpts);
|
||||||
return JsonSerializer.Serialize(items, JsonOpts);
|
|
||||||
default: return value.ToString();
|
default: return value.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,18 +45,25 @@ public static class AttributeValueCodec
|
|||||||
if (elementType is null)
|
if (elementType is null)
|
||||||
throw new FormatException("List attribute requires an element type.");
|
throw new FormatException("List attribute requires an element type.");
|
||||||
|
|
||||||
string?[] raw;
|
JsonElement[] raw;
|
||||||
try { raw = JsonSerializer.Deserialize<string?[]>(value) ?? []; }
|
try { raw = JsonSerializer.Deserialize<JsonElement[]>(value) ?? []; }
|
||||||
catch (JsonException ex) { throw new FormatException("Malformed list JSON.", ex); }
|
catch (JsonException ex) { throw new FormatException("Malformed list JSON.", ex); }
|
||||||
|
|
||||||
var clrType = ElementClrType(elementType.Value);
|
var clrType = ElementClrType(elementType.Value);
|
||||||
var listType = typeof(List<>).MakeGenericType(clrType);
|
var listType = typeof(List<>).MakeGenericType(clrType);
|
||||||
var result = (IList)Activator.CreateInstance(listType)!;
|
var result = (IList)Activator.CreateInstance(listType)!;
|
||||||
foreach (var item in raw)
|
foreach (var el in raw)
|
||||||
result.Add(ParseScalar(item, elementType.Value));
|
result.Add(ParseScalar(JsonElementToString(el), elementType.Value));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string? JsonElementToString(JsonElement el) => el.ValueKind switch
|
||||||
|
{
|
||||||
|
JsonValueKind.String => el.GetString(), // old form, or string-typed lists
|
||||||
|
JsonValueKind.Null => null, // ParseScalar throws "may not be null"
|
||||||
|
_ => el.GetRawText() // number/bool → "10" / "1.5" / "true"
|
||||||
|
};
|
||||||
|
|
||||||
private static Type ElementClrType(DataType t) => t switch
|
private static Type ElementClrType(DataType t) => t switch
|
||||||
{
|
{
|
||||||
DataType.String => typeof(string),
|
DataType.String => typeof(string),
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ public class AttributeValueCodecTests
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
CultureInfo.CurrentCulture = new CultureInfo("de-DE");
|
CultureInfo.CurrentCulture = new CultureInfo("de-DE");
|
||||||
Assert.Equal("[\"1.5\",\"2.5\"]",
|
Assert.Equal("[1.5,2.5]",
|
||||||
AttributeValueCodec.Encode(new List<double> { 1.5, 2.5 }));
|
AttributeValueCodec.Encode(new List<double> { 1.5, 2.5 }));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -73,6 +73,53 @@ public class AttributeValueCodecTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Encode_Int32List_ProducesNativeNumbers() =>
|
||||||
|
Assert.Equal("[10,20,30]",
|
||||||
|
AttributeValueCodec.Encode(new List<int> { 10, 20, 30 }));
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Encode_BoolList_ProducesNativeBooleans() =>
|
||||||
|
Assert.Equal("[true,false]",
|
||||||
|
AttributeValueCodec.Encode(new List<bool> { true, false }));
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Encode_StringList_StaysQuoted() =>
|
||||||
|
Assert.Equal("[\"a\",\"b\"]",
|
||||||
|
AttributeValueCodec.Encode(new List<string> { "a", "b" }));
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Encode_DateTimeList_IsIso8601()
|
||||||
|
{
|
||||||
|
var json = AttributeValueCodec.Encode(
|
||||||
|
new List<DateTime> { new(2026, 6, 16, 0, 0, 0, DateTimeKind.Utc) });
|
||||||
|
Assert.Contains("2026-06-16T00:00:00", json);
|
||||||
|
Assert.DoesNotContain("06/16/2026", json);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Decode_NewNativeIntForm_Parses()
|
||||||
|
{
|
||||||
|
var back = (IList<int>)AttributeValueCodec.Decode("[10,20]", DataType.List, DataType.Int32)!;
|
||||||
|
Assert.Equal(new[] { 10, 20 }, back);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Decode_OldStringIntForm_BackwardCompatible()
|
||||||
|
{
|
||||||
|
var back = (IList<int>)AttributeValueCodec.Decode("[\"10\",\"20\"]", DataType.List, DataType.Int32)!;
|
||||||
|
Assert.Equal(new[] { 10, 20 }, back);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("[true,false]")]
|
||||||
|
[InlineData("[\"True\",\"False\"]")]
|
||||||
|
public void Decode_BoolForms_BothParse(string json)
|
||||||
|
{
|
||||||
|
var back = (IList<bool>)AttributeValueCodec.Decode(json, DataType.List, DataType.Boolean)!;
|
||||||
|
Assert.Equal(new[] { true, false }, back);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RoundTrip_Int32List()
|
public void RoundTrip_Int32List()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user