diff --git a/Doxense.Core.Tests/Doxense.Core.Tests.csproj b/Doxense.Core.Tests/Doxense.Core.Tests.csproj index 7b4e16f5..43a2a434 100644 --- a/Doxense.Core.Tests/Doxense.Core.Tests.csproj +++ b/Doxense.Core.Tests/Doxense.Core.Tests.csproj @@ -25,6 +25,7 @@ + diff --git a/Doxense.Core.Tests/Serialization/JSON/CrystalJson.cs b/Doxense.Core.Tests/Serialization/JSON/CrystalJson.cs index 5ad47693..158daafe 100644 --- a/Doxense.Core.Tests/Serialization/JSON/CrystalJson.cs +++ b/Doxense.Core.Tests/Serialization/JSON/CrystalJson.cs @@ -1,4 +1,4 @@ -#region Copyright (c) 2023-2024 SnowBank SAS, (c) 2005-2023 Doxense SAS +#region Copyright (c) 2023-2024 SnowBank SAS, (c) 2005-2023 Doxense SAS // All rights reserved. // // Redistribution and use in source and binary forms, with or without @@ -91,6 +91,9 @@ namespace Doxense.Serialization.Json.Tests using NodaTime; using NUnit.Framework.Constraints; + using STJ = System.Text.Json; + using NJ = Newtonsoft.Json; + [TestFixture] [Category("Core-SDK")] [Category("Core-JSON")] @@ -3508,6 +3511,75 @@ public void Test_JsonDateTime_TryFormat() Verify_TryFormat(DateOnly.FromDateTime(DateTime.Now)); } + [Test] + public void Test_PropertyNames_CrystalJson() + { + var instance = new DummyCrystalJsonTextPropertyNames() + { + HelloWorld = "hello", // => "helloWorld": "hello" + Foo = "world", // => "bar": "world" + }; + + var json = CrystalJson.Serialize(instance, CrystalJsonSettings.Json); + Log(json); + Assert.That(json, Is.EqualTo("{ \"helloWorld\": \"hello\", \"bar\": \"world\" }")); + + var obj = JsonObject.FromObject(instance); + Dump(obj); + Assert.That(obj["helloWorld"], IsJson.EqualTo("hello")); + Assert.That(obj["bar"], IsJson.EqualTo("world")); + + var decoded = obj.As()!; + Assert.That(decoded.HelloWorld, Is.EqualTo("hello")); + Assert.That(decoded.Foo, Is.EqualTo("world")); + } + + [Test] + public void Test_PropertyNames_SystemTextJson() + { + var instance = new DummySystemJsonTextPropertyNames() + { + HelloWorld = "hello", // => "helloWorld": "hello" + Foo = "world", // => "bar": "world" + }; + + var json = CrystalJson.Serialize(instance, CrystalJsonSettings.Json); + Log(json); + Assert.That(json, Is.EqualTo("{ \"helloWorld\": \"hello\", \"bar\": \"world\" }")); + + var obj = JsonObject.FromObject(instance); + Dump(obj); + Assert.That(obj["helloWorld"], IsJson.EqualTo("hello")); + Assert.That(obj["bar"], IsJson.EqualTo("world")); + + var decoded = obj.As()!; + Assert.That(decoded.HelloWorld, Is.EqualTo("hello")); + Assert.That(decoded.Foo, Is.EqualTo("world")); + } + + [Test] + public void Test_PropertyNames_NewtonsoftJson() + { + var instance = new DummyNewtonsoftJsonPropertyNames() + { + HelloWorld = "hello", // => "helloWorld": "hello" + Foo = "world", // => "bar": "world" + }; + + var json = CrystalJson.Serialize(instance, CrystalJsonSettings.Json); + Log(json); + Assert.That(json, Is.EqualTo("{ \"helloWorld\": \"hello\", \"bar\": \"world\" }")); + + var obj = JsonObject.FromObject(instance); + Dump(obj); + Assert.That(obj["helloWorld"], IsJson.EqualTo("hello")); + Assert.That(obj["bar"], IsJson.EqualTo("world")); + + var decoded = obj.As()!; + Assert.That(decoded.HelloWorld, Is.EqualTo("hello")); + Assert.That(decoded.Foo, Is.EqualTo("world")); + } + #endregion #region JSON Object Model... @@ -10937,6 +11009,39 @@ class DummyXmlSerializableContractClass } #pragma warning restore 649 + public sealed class DummyCrystalJsonTextPropertyNames + { + // test that we recognize our own JsonPropertyAttribute + + [JsonProperty("helloWorld")] + public string? HelloWorld { get; set; } + + [JsonProperty("bar")] + public string? Foo { get; set; } + } + + public sealed class DummySystemJsonTextPropertyNames + { + // test that we recognize System.Text.Json.Serialization.JsonPropertyNameAttribute + + [STJ.Serialization.JsonPropertyName("helloWorld")] + public string? HelloWorld { get; set; } + + [STJ.Serialization.JsonPropertyName("bar")] + public string? Foo { get; set; } + } + + public sealed class DummyNewtonsoftJsonPropertyNames + { + // test that we recognize Newtonsoft.Json.JsonPropertyAttribute + + [NJ.JsonProperty("helloWorld")] + public string? HelloWorld { get; set; } + + [NJ.JsonProperty("bar")] + public string? Foo { get; set; } + } + #endregion } diff --git a/Doxense.Core.Tests/packages.lock.json b/Doxense.Core.Tests/packages.lock.json index 450a08c0..e46fe256 100644 --- a/Doxense.Core.Tests/packages.lock.json +++ b/Doxense.Core.Tests/packages.lock.json @@ -22,6 +22,12 @@ "Newtonsoft.Json": "13.0.1" } }, + "Newtonsoft.Json": { + "type": "Direct", + "requested": "[13.0.3, )", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, "NodaTime.Testing": { "type": "Direct", "requested": "[3.2.0, )", @@ -276,12 +282,6 @@ "Microsoft.Extensions.Primitives": "8.0.0" } }, - "Newtonsoft.Json": { - "type": "CentralTransitive", - "requested": "[13.0.3, )", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, "NodaTime": { "type": "CentralTransitive", "requested": "[3.2.0, )", @@ -360,6 +360,12 @@ "Newtonsoft.Json": "13.0.1" } }, + "Newtonsoft.Json": { + "type": "Direct", + "requested": "[13.0.3, )", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" + }, "NodaTime.Testing": { "type": "Direct", "requested": "[3.2.0, )", @@ -613,12 +619,6 @@ "Microsoft.Extensions.Primitives": "9.0.0" } }, - "Newtonsoft.Json": { - "type": "CentralTransitive", - "requested": "[13.0.3, )", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, "NodaTime": { "type": "CentralTransitive", "requested": "[3.2.0, )", diff --git a/Doxense.Core/Serialization/JSON/Reflection/CrystalJsonTypeResolver.cs b/Doxense.Core/Serialization/JSON/Reflection/CrystalJsonTypeResolver.cs index edd3ad2d..459af3c6 100644 --- a/Doxense.Core/Serialization/JSON/Reflection/CrystalJsonTypeResolver.cs +++ b/Doxense.Core/Serialization/JSON/Reflection/CrystalJsonTypeResolver.cs @@ -988,51 +988,44 @@ private static bool FilterMemberByType(MemberInfo _, Type type) return null; } - private static JsonPropertyAttribute? FindPropertyAttribute(FieldInfo field) + private static JsonPropertyAttribute? FindPropertyAttribute(MemberInfo member) { - System.Text.Json.Serialization.JsonPropertyNameAttribute? fallback = null; - foreach (var attr in field.GetCustomAttributes(true)) + System.Text.Json.Serialization.JsonPropertyNameAttribute? fallbackSystemTextJson = null; + Attribute? fallbackNewtonsoftJson = null; + foreach (var attr in member.GetCustomAttributes(true)) { + // look for our own attribute, that has priority if (attr is JsonPropertyAttribute jp) { return jp; } + // recognize [JsonPropertyName(...)] from System.Text.Json, if present if (attr is System.Text.Json.Serialization.JsonPropertyNameAttribute jpn) { - fallback = jpn; + fallbackSystemTextJson = jpn; } - } - - if (fallback is not null) - { // fake the original [JsonProperty("...")] by copying the name of the other attribute - return new JsonPropertyAttribute(fallback.Name); - } - - return null; - } - - private static JsonPropertyAttribute? FindPropertyAttribute(PropertyInfo prop) - { - System.Text.Json.Serialization.JsonPropertyNameAttribute? fallback = null; - foreach (var attr in prop.GetCustomAttributes(true)) - { - if (attr is JsonPropertyAttribute jp) - { - return jp; - } - - if (attr is System.Text.Json.Serialization.JsonPropertyNameAttribute jpn) + + // likewise, recognize the attribute from JSON.Net + //note: since we don't reference the package, we have to test the name+namespace ! + if (attr.GetType().Name == "JsonPropertyAttribute" && attr.GetType().Namespace == "Newtonsoft.Json") { - fallback = jpn; + fallbackNewtonsoftJson = (Attribute) attr; } } - if (fallback is not null) + if (fallbackSystemTextJson is not null) { // fake the original [JsonProperty("...")] by copying the name of the other attribute - return new JsonPropertyAttribute(fallback.Name); + return new JsonPropertyAttribute(fallbackSystemTextJson.Name); + } + + if (fallbackNewtonsoftJson is not null) + { // we need to access the "PropertyName" property via reflection! + var name = (string?) fallbackNewtonsoftJson.GetType().GetProperty("PropertyName")?.GetValue(fallbackNewtonsoftJson); + if (name != null) return new JsonPropertyAttribute(name); } + // no valid candidate found return null; }