Skip to content

Commit

Permalink
JSON: recognize the [JsonProperty] attribute from Newtonsoft.Json
Browse files Browse the repository at this point in the history
- some libraries uses data types that have the |JsonProperty] attribute from Newtonsoft.Json package, to override the name of the members
- if we find this attribute, extract the PropertyName and use it instead.
- note: we don't directly reference the JSON.Net package at runtime, so the detection looks for the name + namespace, which may break with AoT
  • Loading branch information
KrzysFR committed Dec 12, 2024
1 parent 050b6a9 commit 1d66db9
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 41 deletions.
1 change: 1 addition & 0 deletions Doxense.Core.Tests/Doxense.Core.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Microsoft.TestPlatform.TestHost" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="NodaTime.Testing" />
<PackageReference Include="NUnit" />
<PackageReference Include="NUnit.Analyzers">
Expand Down
107 changes: 106 additions & 1 deletion Doxense.Core.Tests/Serialization/JSON/CrystalJson.cs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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")]
Expand Down Expand Up @@ -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<DummyCrystalJsonTextPropertyNames>()!;
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<DummySystemJsonTextPropertyNames>()!;
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<DummyNewtonsoftJsonPropertyNames>()!;
Assert.That(decoded.HelloWorld, Is.EqualTo("hello"));
Assert.That(decoded.Foo, Is.EqualTo("world"));
}

#endregion

#region JSON Object Model...
Expand Down Expand Up @@ -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

}
24 changes: 12 additions & 12 deletions Doxense.Core.Tests/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -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, )",
Expand Down Expand Up @@ -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, )",
Expand Down Expand Up @@ -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, )",
Expand Down Expand Up @@ -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, )",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down

0 comments on commit 1d66db9

Please sign in to comment.