-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add json support for F# options, lists, sets, maps and records (#55108)
* 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
1 parent
54d4f62
commit dc2fe34
Showing
23 changed files
with
1,475 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
75 changes: 75 additions & 0 deletions
75
...tem.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpListConverter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
91 changes: 91 additions & 0 deletions
91
...stem.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpMapConverter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
100 changes: 100 additions & 0 deletions
100
...m.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
Oops, something went wrong.