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 all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions Src/Newtonsoft.Json.Tests/Issues/Issue1295.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#region License
// Copyright (c) 2007 James Newton-King
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
#endregion

using Newtonsoft.Json.Tests.TestObjects;
#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);
#if !NET20
Assert.AreEqual(@"{""foo_field"":""42""}", asJson);
T-Gro marked this conversation as resolved.
Show resolved Hide resolved
#else
Assert.AreEqual(@"{""Foo"":""42""}", asJson);
#endif

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#region License
// Copyright (c) 2007 James Newton-King
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
#endregion

using System;
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 + " }";
}
}
}
13 changes: 9 additions & 4 deletions Src/Newtonsoft.Json/Serialization/DefaultContractResolver.cs
Original file line number Diff line number Diff line change
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,11 @@ 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) ??
// In case of name-mapping attributes such as DataMember, match the underlying name as a constructor param.
// This is needed for types with readonly properties, such as F# records.
properties.GetByUnderlyingName(name);
// must match type as well as name
if (property == null || property.PropertyType != type)
{
Expand Down
19 changes: 19 additions & 0 deletions Src/Newtonsoft.Json/Serialization/JsonPropertyCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,5 +186,24 @@ 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;
}
}

return null;
}
}
}