Skip to content

Commit

Permalink
Add json support for F# options, lists, sets, maps and records (#55108)
Browse files Browse the repository at this point in the history
* Add json support for F# options, lists, sets, maps and records

* fix ILLink warnings

* address ILLink annotations feedback

* add support for ValueOption

* revert unneeded sln changes

* add JsonIgnoreCondition tests for optional types

* Revert "revert unneeded sln changes"

This reverts commit 2e793422dca84bd22d55cdfa2cd6c9b6c5d4963e.

* remove lock from singleton initialization

* improve FSharp.Core missing member error mesages

* throw NotSupportedException on discriminated unions

* extend optional test coverage to include list, set and map payloads

* simplify changes required to converter infrastructure
  • Loading branch information
eiriktsarpalis authored Jul 15, 2021
1 parent 54d4f62 commit dc2fe34
Show file tree
Hide file tree
Showing 23 changed files with 1,475 additions and 37 deletions.
55 changes: 31 additions & 24 deletions src/libraries/System.Text.Json/System.Text.Json.sln
Original file line number Diff line number Diff line change
Expand Up @@ -35,35 +35,17 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{676B6044-FA4
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{74017ACD-3AC1-4BB5-804B-D57E305FFBD9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.SourceGeneration", "gen\System.Text.Json.SourceGeneration.csproj", "{6485EED4-C313-4551-9865-8ADCED603629}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration", "gen\System.Text.Json.SourceGeneration.csproj", "{6485EED4-C313-4551-9865-8ADCED603629}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.Tests", "tests\System.Text.Json.Tests\System.Text.Json.Tests.csproj", "{A0178BAA-A1AF-4C69-8E4A-A700A2723DDC}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.Tests", "tests\System.Text.Json.Tests\System.Text.Json.Tests.csproj", "{A0178BAA-A1AF-4C69-8E4A-A700A2723DDC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.SourceGeneration.Tests", "tests\System.Text.Json.SourceGeneration.Tests\System.Text.Json.SourceGeneration.Tests.csproj", "{33599A6C-F340-4E1B-9B4D-CB8946C22140}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.Tests", "tests\System.Text.Json.SourceGeneration.Tests\System.Text.Json.SourceGeneration.Tests.csproj", "{33599A6C-F340-4E1B-9B4D-CB8946C22140}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "System.Text.Json.SourceGeneration.UnitTests", "tests\System.Text.Json.SourceGeneration.UnitTests\System.Text.Json.SourceGeneration.UnitTests.csproj", "{18173CEC-895F-4F62-B7BB-B724457FEDCD}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Text.Json.SourceGeneration.UnitTests", "tests\System.Text.Json.SourceGeneration.UnitTests\System.Text.Json.SourceGeneration.UnitTests.csproj", "{18173CEC-895F-4F62-B7BB-B724457FEDCD}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "System.Text.Json.FSharp.Tests", "tests\System.Text.Json.FSharp.Tests\System.Text.Json.FSharp.Tests.fsproj", "{5720BF06-2031-4AD8-B9B4-31A01E27ABB8}"
EndProject
Global
GlobalSection(NestedProjects) = preSolution
{102945CA-3736-4B2C-8E68-242A0B247F2B} = {3C544454-BD8B-44F4-A174-B61F18957613}
{73D5739C-E382-4E22-A7D3-B82705C58C74} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB}
{25C42754-B384-4842-8FA7-75D7A79ADF0D} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB}
{4774F56D-16A8-4ABB-8C73-5F57609F1773} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB}
{E2077991-EB83-471C-B17F-72F569FFCE6D} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB}
{BE230195-2A1C-4674-BACB-502C2CD864E9} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB}
{D7276D7D-F117-47C5-B514-8E3E964769BE} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB}
{7015E94D-D20D-48C8-86D7-6A996BE99E0E} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB}
{E9AA0AEB-AEAE-4B28-8D4D-17A6D7C89D17} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423}
{1C8262DB-7355-40A8-A2EC-4EED7363134A} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423}
{D05FD93A-BC51-466E-BD56-3F3D6BBE6B06} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423}
{7909EB27-0D6E-46E6-B9F9-8A1EFD557018} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423}
{9BCCDA15-8907-4AE3-8871-2F17775DDE4C} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423}
{1285FF43-F491-4BE0-B92C-37DA689CBD4B} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423}
{6485EED4-C313-4551-9865-8ADCED603629} = {74017ACD-3AC1-4BB5-804B-D57E305FFBD9}
{A0178BAA-A1AF-4C69-8E4A-A700A2723DDC} = {3C544454-BD8B-44F4-A174-B61F18957613}
{33599A6C-F340-4E1B-9B4D-CB8946C22140} = {3C544454-BD8B-44F4-A174-B61F18957613}
{18173CEC-895F-4F62-B7BB-B724457FEDCD} = {3C544454-BD8B-44F4-A174-B61F18957613}
EndGlobalSection
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
Expand Down Expand Up @@ -141,10 +123,35 @@ Global
{18173CEC-895F-4F62-B7BB-B724457FEDCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{18173CEC-895F-4F62-B7BB-B724457FEDCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{18173CEC-895F-4F62-B7BB-B724457FEDCD}.Release|Any CPU.Build.0 = Release|Any CPU
{5720BF06-2031-4AD8-B9B4-31A01E27ABB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5720BF06-2031-4AD8-B9B4-31A01E27ABB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5720BF06-2031-4AD8-B9B4-31A01E27ABB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5720BF06-2031-4AD8-B9B4-31A01E27ABB8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{102945CA-3736-4B2C-8E68-242A0B247F2B} = {3C544454-BD8B-44F4-A174-B61F18957613}
{73D5739C-E382-4E22-A7D3-B82705C58C74} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB}
{E9AA0AEB-AEAE-4B28-8D4D-17A6D7C89D17} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423}
{25C42754-B384-4842-8FA7-75D7A79ADF0D} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB}
{1C8262DB-7355-40A8-A2EC-4EED7363134A} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423}
{4774F56D-16A8-4ABB-8C73-5F57609F1773} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB}
{D05FD93A-BC51-466E-BD56-3F3D6BBE6B06} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423}
{E2077991-EB83-471C-B17F-72F569FFCE6D} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB}
{7909EB27-0D6E-46E6-B9F9-8A1EFD557018} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423}
{BE230195-2A1C-4674-BACB-502C2CD864E9} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB}
{D7276D7D-F117-47C5-B514-8E3E964769BE} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB}
{9BCCDA15-8907-4AE3-8871-2F17775DDE4C} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423}
{7015E94D-D20D-48C8-86D7-6A996BE99E0E} = {EC8CE194-261A-4115-9582-E2DB1A25CAFB}
{1285FF43-F491-4BE0-B92C-37DA689CBD4B} = {676B6044-FA47-4B7D-AEC2-FA94DB23A423}
{6485EED4-C313-4551-9865-8ADCED603629} = {74017ACD-3AC1-4BB5-804B-D57E305FFBD9}
{A0178BAA-A1AF-4C69-8E4A-A700A2723DDC} = {3C544454-BD8B-44F4-A174-B61F18957613}
{33599A6C-F340-4E1B-9B4D-CB8946C22140} = {3C544454-BD8B-44F4-A174-B61F18957613}
{18173CEC-895F-4F62-B7BB-B724457FEDCD} = {3C544454-BD8B-44F4-A174-B61F18957613}
{5720BF06-2031-4AD8-B9B4-31A01E27ABB8} = {3C544454-BD8B-44F4-A174-B61F18957613}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5868B757-D821-41FC-952E-2113A0519506}
EndGlobalSection
Expand Down
8 changes: 7 additions & 1 deletion src/libraries/System.Text.Json/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -611,4 +611,10 @@
<data name="FieldCannotBeVirtual" xml:space="preserve">
<value>A 'field' member cannot be 'virtual'. See arguments for the '{0}' and '{1}' parameters. </value>
</data>
</root>
<data name="MissingFSharpCoreMember" xml:space="preserve">
<value>Could not locate required member '{0}' from FSharp.Core. This might happen because your application has enabled member-level trimming.</value>
</data>
<data name="FSharpDiscriminatedUnionsNotSupported" xml:space="preserve">
<value>F# discriminated union serialization is not supported. Consider authoring a custom converter for the type.</value>
</data>
</root>
7 changes: 7 additions & 0 deletions src/libraries/System.Text.Json/src/System.Text.Json.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
<Compile Include="System\Text\Json\Serialization\IJsonOnSerialized.cs" />
<Compile Include="System\Text\Json\Serialization\IJsonOnSerializing.cs" />
<Compile Include="System\Text\Json\Serialization\JsonSerializerContext.cs" />
<Compile Include="System\Text\Json\Serialization\Metadata\FSharpCoreReflectionProxy.cs" />
<Compile Include="System\Text\Json\Serialization\Metadata\JsonMetadataServices.Collections.cs" />
<Compile Include="System\Text\Json\Serialization\Metadata\JsonMetadataServices.Converters.cs" />
<Compile Include="System\Text\Json\Serialization\Metadata\JsonTypeInfoInternalOfT.cs" />
Expand Down Expand Up @@ -132,6 +133,12 @@
<Compile Include="System\Text\Json\Serialization\Converters\Collection\ListOfTConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Collection\QueueOfTConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Collection\StackOfTConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\FSharp\FSharpListConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\FSharp\FSharpMapConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\FSharp\FSharpSetConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\FSharp\FSharpTypeConverterFactory.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\FSharp\FSharpOptionConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\FSharp\FSharpValueOptionConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Node\JsonArrayConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Node\JsonNodeConverter.cs" />
<Compile Include="System\Text\Json\Serialization\Converters\Node\JsonNodeConverterFactory.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization.Metadata;

namespace System.Text.Json.Serialization.Converters
{
// Converter for F# lists: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-list-1.html
internal sealed class FSharpListConverter<TList, TElement> : IEnumerableDefaultConverter<TList, TElement>
where TList : IEnumerable<TElement>
{
private readonly Func<IEnumerable<TElement>, TList> _listConstructor;

[RequiresUnreferencedCode(FSharpCoreReflectionProxy.FSharpCoreUnreferencedCodeMessage)]
public FSharpListConverter()
{
_listConstructor = FSharpCoreReflectionProxy.Instance.CreateFSharpListConstructor<TList, TElement>();
}

protected override void Add(in TElement value, ref ReadStack state)
{
((List<TElement>)state.Current.ReturnValue!).Add(value);
}

protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options)
{
state.Current.ReturnValue = new List<TElement>();
}

protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options)
{
state.Current.ReturnValue = _listConstructor((List<TElement>)state.Current.ReturnValue!);
}

protected override bool OnWriteResume(Utf8JsonWriter writer, TList value, JsonSerializerOptions options, ref WriteStack state)
{
IEnumerator<TElement> enumerator;
if (state.Current.CollectionEnumerator == null)
{
enumerator = value.GetEnumerator();
if (!enumerator.MoveNext())
{
enumerator.Dispose();
return true;
}
}
else
{
enumerator = (IEnumerator<TElement>)state.Current.CollectionEnumerator;
}

JsonConverter<TElement> converter = GetElementConverter(ref state);
do
{
if (ShouldFlush(writer, ref state))
{
state.Current.CollectionEnumerator = enumerator;
return false;
}

TElement element = enumerator.Current;
if (!converter.TryWrite(writer, element, options, ref state))
{
state.Current.CollectionEnumerator = enumerator;
return false;
}
} while (enumerator.MoveNext());

enumerator.Dispose();
return true;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization.Metadata;

namespace System.Text.Json.Serialization.Converters
{
// Converter for F# maps: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-fsharpmap-2.html
internal sealed class FSharpMapConverter<TMap, TKey, TValue> : DictionaryDefaultConverter<TMap, TKey, TValue>
where TMap : IEnumerable<KeyValuePair<TKey, TValue>>
where TKey : notnull
{
private readonly Func<IEnumerable<Tuple<TKey, TValue>>, TMap> _mapConstructor;

[RequiresUnreferencedCode(FSharpCoreReflectionProxy.FSharpCoreUnreferencedCodeMessage)]
public FSharpMapConverter()
{
_mapConstructor = FSharpCoreReflectionProxy.Instance.CreateFSharpMapConstructor<TMap, TKey, TValue>();
}

protected override void Add(TKey key, in TValue value, JsonSerializerOptions options, ref ReadStack state)
{
((List<Tuple<TKey, TValue>>)state.Current.ReturnValue!).Add (new Tuple<TKey, TValue>(key, value));
}

internal override bool CanHaveIdMetadata => false;

protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state)
{
state.Current.ReturnValue = new List<Tuple<TKey, TValue>>();
}

protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options)
{
state.Current.ReturnValue = _mapConstructor((List<Tuple<TKey, TValue>>)state.Current.ReturnValue!);
}

protected internal override bool OnWriteResume(Utf8JsonWriter writer, TMap value, JsonSerializerOptions options, ref WriteStack state)
{
IEnumerator<KeyValuePair<TKey, TValue>> enumerator;
if (state.Current.CollectionEnumerator == null)
{
enumerator = value.GetEnumerator();
if (!enumerator.MoveNext())
{
enumerator.Dispose();
return true;
}
}
else
{
enumerator = (IEnumerator<KeyValuePair<TKey, TValue>>)state.Current.CollectionEnumerator;
}

JsonTypeInfo typeInfo = state.Current.JsonTypeInfo;
_keyConverter ??= GetConverter<TKey>(typeInfo.KeyTypeInfo!);
_valueConverter ??= GetConverter<TValue>(typeInfo.ElementTypeInfo!);

do
{
if (ShouldFlush(writer, ref state))
{
state.Current.CollectionEnumerator = enumerator;
return false;
}

if (state.Current.PropertyState < StackFramePropertyState.Name)
{
state.Current.PropertyState = StackFramePropertyState.Name;

TKey key = enumerator.Current.Key;
_keyConverter.WriteWithQuotes(writer, key, options, ref state);
}

TValue element = enumerator.Current.Value;
if (!_valueConverter.TryWrite(writer, element, options, ref state))
{
state.Current.CollectionEnumerator = enumerator;
return false;
}

state.Current.EndDictionaryElement();
} while (enumerator.MoveNext());

enumerator.Dispose();
return true;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization.Metadata;

namespace System.Text.Json.Serialization.Converters
{
// Converter for F# optional values: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-core-option-1.html
// Serializes `Some(value)` using the format of `value` and `None` values as `null`.
internal sealed class FSharpOptionConverter<TOption, TElement> : JsonConverter<TOption>
where TOption : class
{
// Reflect the converter strategy of the element type, since we use the identical contract for Some(_) values.
internal override ConverterStrategy ConverterStrategy => _converterStrategy;
internal override Type? ElementType => typeof(TElement);
// 'None' is encoded using 'null' at runtime and serialized as 'null' in JSON.
public override bool HandleNull => true;

private readonly JsonConverter<TElement> _elementConverter;
private readonly Func<TOption, TElement> _optionValueGetter;
private readonly Func<TElement?, TOption> _optionConstructor;
private readonly ConverterStrategy _converterStrategy;

[RequiresUnreferencedCode(FSharpCoreReflectionProxy.FSharpCoreUnreferencedCodeMessage)]
public FSharpOptionConverter(JsonConverter<TElement> elementConverter)
{
_elementConverter = elementConverter;
_optionValueGetter = FSharpCoreReflectionProxy.Instance.CreateFSharpOptionValueGetter<TOption, TElement>();
_optionConstructor = FSharpCoreReflectionProxy.Instance.CreateFSharpOptionSomeConstructor<TOption, TElement>();

// temporary workaround for JsonConverter base constructor needing to access
// ConverterStrategy when calculating `CanUseDirectReadOrWrite`.
// TODO move `CanUseDirectReadOrWrite` from JsonConverter to JsonTypeInfo.
_converterStrategy = _elementConverter.ConverterStrategy;
CanUseDirectReadOrWrite = _converterStrategy == ConverterStrategy.Value;
}

internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, ref ReadStack state, out TOption? value)
{
// `null` values deserialize as `None`
if (!state.IsContinuation && reader.TokenType == JsonTokenType.Null)
{
value = null;
return true;
}

state.Current.JsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
if (_elementConverter.TryRead(ref reader, typeof(TElement), options, ref state, out TElement? element))
{
value = _optionConstructor(element);
return true;
}

value = null;
return false;
}

internal override bool OnTryWrite(Utf8JsonWriter writer, TOption value, JsonSerializerOptions options, ref WriteStack state)
{
if (value is null)
{
// Write `None` values as null
writer.WriteNullValue();
return true;
}

TElement element = _optionValueGetter(value);
state.Current.DeclaredJsonPropertyInfo = state.Current.JsonTypeInfo.ElementTypeInfo!.PropertyInfoForTypeInfo;
return _elementConverter.TryWrite(writer, element, options, ref state);
}

// Since this is a hybrid converter (ConverterStrategy depends on the element converter),
// we need to override the value converter Write and Read methods too.

public override void Write(Utf8JsonWriter writer, TOption value, JsonSerializerOptions options)
{
if (value is null)
{
writer.WriteNullValue();
}
else
{
TElement element = _optionValueGetter(value);
_elementConverter.Write(writer, element, options);
}
}

public override TOption? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
{
return null;
}

TElement? element = _elementConverter.Read(ref reader, typeToConvert, options);
return _optionConstructor(element);
}
}
}
Loading

0 comments on commit dc2fe34

Please sign in to comment.