Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable roundtrip for F# struct record with datamember attribute #2860

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 34 additions & 0 deletions Src/Newtonsoft.Json.Tests/Issues/Issue1295.cs
@@ -0,0 +1,34 @@
using Newtonsoft.Json.Tests.TestObjects;
T-Gro marked this conversation as resolved.
Show resolved Hide resolved
#if DNXCORE50
using Xunit;
using Test = Xunit.FactAttribute;
using Assert = Newtonsoft.Json.Tests.XUnitAssert;
#else
using NUnit.Framework;
#endif

namespace Newtonsoft.Json.Tests.Issues
{
[TestFixture]
public class Issue1295 : TestFixtureBase
{
[Test]
public void Test()
{
var fsharpStructRecord = new FSharpStructRecordWithDataMember("Hi there");
var asJson = JsonConvert.SerializeObject(fsharpStructRecord);
var deserialized = JsonConvert.DeserializeObject<FSharpStructRecordWithDataMember>(asJson);

Assert.AreEqual(fsharpStructRecord.Foo, deserialized.Foo);
}

[Test]
public void TestJsonSanity()
{
var fsharpStructRecord = new FSharpStructRecordWithDataMember("42");
var asJson = JsonConvert.SerializeObject(fsharpStructRecord);

Assert.AreEqual(@"{""foo_field"":""42""}", asJson);
T-Gro marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
@@ -0,0 +1,45 @@
using System;
T-Gro marked this conversation as resolved.
Show resolved Hide resolved
using System.Diagnostics;
using System.Runtime.CompilerServices;
#if !NET20
using System.Runtime.Serialization;
#endif

namespace Newtonsoft.Json.Tests.TestObjects
{
#if !NET20
[Serializable]
[DataContract]
#endif
// This mimics what F# compiler produces for a simple [<Struct>] record which is also using DataMember attribute
// Follows https://github.com/JamesNK/Newtonsoft.Json/issues/1295#issuecomment-1534807350 , simplified to have it compile
public struct FSharpStructRecordWithDataMember
{
[CompilerGenerated]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
internal string Foo_;
#if !NET20
[DataMember(Name = "foo_field")]
#endif
public readonly string Foo
{
[CompilerGenerated]
[DebuggerNonUserCode]
get
{
return Foo_;
}
}

public FSharpStructRecordWithDataMember(string foo)
{
Foo_ = foo;
}

[CompilerGenerated]
public override string ToString()
{
return "FSharpStructRecordWithDataMember { Foo = " + Foo + " }";
}
}
}
11 changes: 7 additions & 4 deletions Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs
Expand Up @@ -640,7 +640,8 @@ IEnumerator IEnumerable.GetEnumerator()
{
foreach (ParameterInfo parameterInfo in parameters)
{
JsonProperty? memberProperty = MatchProperty(memberProperties, parameterInfo.Name!, parameterInfo.ParameterType);
JsonProperty? memberProperty =
MatchConstructorProperty(memberProperties, parameterInfo.Name!, parameterInfo.ParameterType);
if (memberProperty == null || memberProperty.Writable)
{
return null;
Expand Down Expand Up @@ -697,7 +698,7 @@ protected virtual IList<JsonProperty> CreateConstructorParameters(ConstructorInf
continue;
}

JsonProperty? matchingMemberProperty = MatchProperty(memberProperties, parameterInfo.Name, parameterInfo.ParameterType);
JsonProperty? matchingMemberProperty = MatchConstructorProperty(memberProperties, parameterInfo.Name, parameterInfo.ParameterType);

// ensure that property will have a name from matching property or from parameterinfo
// parameterinfo could have no name if generated by a proxy (I'm looking at you Castle)
Expand All @@ -715,7 +716,7 @@ protected virtual IList<JsonProperty> CreateConstructorParameters(ConstructorInf
return parameterCollection;
}

private JsonProperty? MatchProperty(JsonPropertyCollection properties, string name, Type type)
private JsonProperty? MatchConstructorProperty(JsonPropertyCollection properties, string name, Type type)
{
// it is possible to generate a member with a null name using Reflection.Emit
// protect against an ArgumentNullException from GetClosestMatchProperty by testing for null here
Expand All @@ -724,7 +725,9 @@ protected virtual IList<JsonProperty> CreateConstructorParameters(ConstructorInf
return null;
}

JsonProperty? property = properties.GetClosestMatchProperty(name);
JsonProperty? property =
T-Gro marked this conversation as resolved.
Show resolved Hide resolved
properties.GetClosestMatchProperty(name) ??
properties.GetByUnderlyingName(name);
// must match type as well as name
if (property == null || property.PropertyType != type)
{
Expand Down
19 changes: 18 additions & 1 deletion Src/Newtonsoft.Json/Serialization/JsonPropertyCollection.cs
Expand Up @@ -135,7 +135,7 @@ public void AddProperty(JsonProperty property)
/// <param name="propertyName">Name of the property.</param>
/// <returns>A matching property if found.</returns>
public JsonProperty? GetClosestMatchProperty(string propertyName)
{
{
T-Gro marked this conversation as resolved.
Show resolved Hide resolved
JsonProperty? property = GetProperty(propertyName, StringComparison.Ordinal);
if (property == null)
{
Expand Down Expand Up @@ -186,5 +186,22 @@ private bool TryGetProperty(string key, [NotNullWhen(true)]out JsonProperty? ite

return null;
}

/// <summary>
/// Gets a property by underlying name. This makes a difference when using name-mapping attributes such as DataMember
/// </summary>
/// <param name="propertyName">The name of the property to get.</param>
/// <returns>A matching property if found.</returns>
public JsonProperty? GetByUnderlyingName(string propertyName)
{
for (int i = 0; i < _list.Count; i++)
{
JsonProperty property = _list[i];
if (string.Equals(propertyName, property.UnderlyingName, StringComparison.OrdinalIgnoreCase))
return property;
T-Gro marked this conversation as resolved.
Show resolved Hide resolved
}

return null;
}
}
}