-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Add json support for F# options, lists, sets, maps and records #55108
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
Merged
eiriktsarpalis
merged 12 commits into
dotnet:main
from
eiriktsarpalis:json-fsharp-support
Jul 15, 2021
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
a8f594e
Add json support for F# options, lists, sets, maps and records
eiriktsarpalis 2e1d449
fix ILLink warnings
eiriktsarpalis d9cc505
address ILLink annotations feedback
eiriktsarpalis ed8a0a5
add support for ValueOption
eiriktsarpalis e666c4f
revert unneeded sln changes
eiriktsarpalis 12de895
add JsonIgnoreCondition tests for optional types
eiriktsarpalis 967dd50
Revert "revert unneeded sln changes"
eiriktsarpalis e61a8da
remove lock from singleton initialization
eiriktsarpalis 4e68e18
improve FSharp.Core missing member error mesages
eiriktsarpalis 74a58f3
throw NotSupportedException on discriminated unions
eiriktsarpalis 987e340
extend optional test coverage to include list, set and map payloads
eiriktsarpalis 17a76a0
simplify changes required to converter infrastructure
eiriktsarpalis File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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 hidden or 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)) | ||
| { | ||
eiriktsarpalis marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.