diff --git a/src/libraries/System.Text.Json/System.Text.Json.sln b/src/libraries/System.Text.Json/System.Text.Json.sln index 433c378f98852a..7e8af66bc59f81 100644 --- a/src/libraries/System.Text.Json/System.Text.Json.sln +++ b/src/libraries/System.Text.Json/System.Text.Json.sln @@ -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 @@ -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 diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index e654d368051dc2..14f5e6b50a60c3 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -611,4 +611,10 @@ A 'field' member cannot be 'virtual'. See arguments for the '{0}' and '{1}' parameters. - \ No newline at end of file + + Could not locate required member '{0}' from FSharp.Core. This might happen because your application has enabled member-level trimming. + + + F# discriminated union serialization is not supported. Consider authoring a custom converter for the type. + + diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index 08855731abe63e..7d428ffb19484c 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -97,6 +97,7 @@ + @@ -132,6 +133,12 @@ + + + + + + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpListConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpListConverter.cs new file mode 100644 index 00000000000000..036d04e8bebf19 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpListConverter.cs @@ -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 : IEnumerableDefaultConverter + where TList : IEnumerable + { + private readonly Func, TList> _listConstructor; + + [RequiresUnreferencedCode(FSharpCoreReflectionProxy.FSharpCoreUnreferencedCodeMessage)] + public FSharpListConverter() + { + _listConstructor = FSharpCoreReflectionProxy.Instance.CreateFSharpListConstructor(); + } + + protected override void Add(in TElement value, ref ReadStack state) + { + ((List)state.Current.ReturnValue!).Add(value); + } + + protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options) + { + state.Current.ReturnValue = new List(); + } + + protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options) + { + state.Current.ReturnValue = _listConstructor((List)state.Current.ReturnValue!); + } + + protected override bool OnWriteResume(Utf8JsonWriter writer, TList value, JsonSerializerOptions options, ref WriteStack state) + { + IEnumerator enumerator; + if (state.Current.CollectionEnumerator == null) + { + enumerator = value.GetEnumerator(); + if (!enumerator.MoveNext()) + { + enumerator.Dispose(); + return true; + } + } + else + { + enumerator = (IEnumerator)state.Current.CollectionEnumerator; + } + + JsonConverter 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; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpMapConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpMapConverter.cs new file mode 100644 index 00000000000000..30d16a09df84e2 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpMapConverter.cs @@ -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 : DictionaryDefaultConverter + where TMap : IEnumerable> + where TKey : notnull + { + private readonly Func>, TMap> _mapConstructor; + + [RequiresUnreferencedCode(FSharpCoreReflectionProxy.FSharpCoreUnreferencedCodeMessage)] + public FSharpMapConverter() + { + _mapConstructor = FSharpCoreReflectionProxy.Instance.CreateFSharpMapConstructor(); + } + + protected override void Add(TKey key, in TValue value, JsonSerializerOptions options, ref ReadStack state) + { + ((List>)state.Current.ReturnValue!).Add (new Tuple(key, value)); + } + + internal override bool CanHaveIdMetadata => false; + + protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state) + { + state.Current.ReturnValue = new List>(); + } + + protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options) + { + state.Current.ReturnValue = _mapConstructor((List>)state.Current.ReturnValue!); + } + + protected internal override bool OnWriteResume(Utf8JsonWriter writer, TMap value, JsonSerializerOptions options, ref WriteStack state) + { + IEnumerator> enumerator; + if (state.Current.CollectionEnumerator == null) + { + enumerator = value.GetEnumerator(); + if (!enumerator.MoveNext()) + { + enumerator.Dispose(); + return true; + } + } + else + { + enumerator = (IEnumerator>)state.Current.CollectionEnumerator; + } + + JsonTypeInfo typeInfo = state.Current.JsonTypeInfo; + _keyConverter ??= GetConverter(typeInfo.KeyTypeInfo!); + _valueConverter ??= GetConverter(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; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs new file mode 100644 index 00000000000000..a02dd449a9da89 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpOptionConverter.cs @@ -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 : JsonConverter + 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 _elementConverter; + private readonly Func _optionValueGetter; + private readonly Func _optionConstructor; + private readonly ConverterStrategy _converterStrategy; + + [RequiresUnreferencedCode(FSharpCoreReflectionProxy.FSharpCoreUnreferencedCodeMessage)] + public FSharpOptionConverter(JsonConverter elementConverter) + { + _elementConverter = elementConverter; + _optionValueGetter = FSharpCoreReflectionProxy.Instance.CreateFSharpOptionValueGetter(); + _optionConstructor = FSharpCoreReflectionProxy.Instance.CreateFSharpOptionSomeConstructor(); + + // 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); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpSetConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpSetConverter.cs new file mode 100644 index 00000000000000..35c464643a3144 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpSetConverter.cs @@ -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# sets: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-fsharpset-1.html + internal sealed class FSharpSetConverter : IEnumerableDefaultConverter + where TSet : IEnumerable + { + private readonly Func, TSet> _setConstructor; + + [RequiresUnreferencedCode(FSharpCoreReflectionProxy.FSharpCoreUnreferencedCodeMessage)] + public FSharpSetConverter() + { + _setConstructor = FSharpCoreReflectionProxy.Instance.CreateFSharpSetConstructor(); + } + + protected override void Add(in TElement value, ref ReadStack state) + { + ((List)state.Current.ReturnValue!).Add(value); + } + + protected override void CreateCollection(ref Utf8JsonReader reader, ref ReadStack state, JsonSerializerOptions options) + { + state.Current.ReturnValue = new List(); + } + + protected override void ConvertCollection(ref ReadStack state, JsonSerializerOptions options) + { + state.Current.ReturnValue = _setConstructor((List)state.Current.ReturnValue!); + } + + protected override bool OnWriteResume(Utf8JsonWriter writer, TSet value, JsonSerializerOptions options, ref WriteStack state) + { + IEnumerator enumerator; + if (state.Current.CollectionEnumerator == null) + { + enumerator = value.GetEnumerator(); + if (!enumerator.MoveNext()) + { + enumerator.Dispose(); + return true; + } + } + else + { + enumerator = (IEnumerator)state.Current.CollectionEnumerator; + } + + JsonConverter 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; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs new file mode 100644 index 00000000000000..c0cb9b88fd5743 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpTypeConverterFactory.cs @@ -0,0 +1,77 @@ +// 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; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization.Metadata; +using FSharpKind = System.Text.Json.Serialization.Metadata.FSharpCoreReflectionProxy.FSharpKind; + +namespace System.Text.Json.Serialization.Converters +{ + internal class FSharpTypeConverterFactory : JsonConverterFactory + { + // Temporary solution to account for not implemented support for type-level attributes + // TODO remove once addressed https://github.com/mono/linker/issues/1742#issuecomment-875036480 + [RequiresUnreferencedCode(FSharpCoreReflectionProxy.FSharpCoreUnreferencedCodeMessage)] + public FSharpTypeConverterFactory() { } + + private ObjectConverterFactory? _recordConverterFactory; + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", + Justification = "The ctor is marked with RequiresUnreferencedCode.")] + public override bool CanConvert(Type typeToConvert) => + FSharpCoreReflectionProxy.IsFSharpType(typeToConvert) && + FSharpCoreReflectionProxy.Instance.DetectFSharpKind(typeToConvert) is not FSharpKind.Unrecognized; + + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", + Justification = "The ctor is marked with RequiresUnreferencedCode.")] + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + Debug.Assert(CanConvert(typeToConvert)); + + Type elementType; + Type converterFactoryType; + object?[]? constructorArguments = null; + + switch (FSharpCoreReflectionProxy.Instance.DetectFSharpKind(typeToConvert)) + { + case FSharpKind.Option: + elementType = typeToConvert.GetGenericArguments()[0]; + converterFactoryType = typeof(FSharpOptionConverter<,>).MakeGenericType(typeToConvert, elementType); + constructorArguments = new object[] { options.GetConverterInternal(elementType) }; + break; + case FSharpKind.ValueOption: + elementType = typeToConvert.GetGenericArguments()[0]; + converterFactoryType = typeof(FSharpValueOptionConverter<,>).MakeGenericType(typeToConvert, elementType); + constructorArguments = new object[] { options.GetConverterInternal(elementType) }; + break; + case FSharpKind.List: + elementType = typeToConvert.GetGenericArguments()[0]; + converterFactoryType = typeof(FSharpListConverter<,>).MakeGenericType(typeToConvert, elementType); + break; + case FSharpKind.Set: + elementType = typeToConvert.GetGenericArguments()[0]; + converterFactoryType = typeof(FSharpSetConverter<,>).MakeGenericType(typeToConvert, elementType); + break; + case FSharpKind.Map: + Type[] genericArgs = typeToConvert.GetGenericArguments(); + Type keyType = genericArgs[0]; + Type valueType = genericArgs[1]; + converterFactoryType = typeof(FSharpMapConverter<,,>).MakeGenericType(typeToConvert, keyType, valueType); + break; + case FSharpKind.Record: + // Use a modified object converter factory that picks the right constructor for struct record deserialization. + ObjectConverterFactory objectFactory = _recordConverterFactory ??= new ObjectConverterFactory(useDefaultConstructorInUnannotatedStructs: false); + Debug.Assert(objectFactory.CanConvert(typeToConvert)); + return objectFactory.CreateConverter(typeToConvert, options); + case FSharpKind.Union: + throw new NotSupportedException(SR.FSharpDiscriminatedUnionsNotSupported); + default: + Debug.Fail("Unrecognized F# type."); + throw new Exception(); + } + + return (JsonConverter)Activator.CreateInstance(converterFactoryType, constructorArguments)!; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpValueOptionConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpValueOptionConverter.cs new file mode 100644 index 00000000000000..35d1640d88c4c3 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/FSharp/FSharpValueOptionConverter.cs @@ -0,0 +1,102 @@ +// 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# struct optional values: https://fsharp.github.io/fsharp-core-docs/reference/fsharp-core-fsharpvalueoption-1.html + // Serializes `ValueSome(value)` using the format of `value` and `ValueNone` values as `null`. + internal sealed class FSharpValueOptionConverter : JsonConverter + where TValueOption : struct, IEquatable + { + // Reflect the converter strategy of the element type, since we use the identical contract for ValueSome(_) values. + internal override ConverterStrategy ConverterStrategy => _converterStrategy; + internal override Type? ElementType => typeof(TElement); + // 'ValueNone' is encoded using 'default' at runtime and serialized as 'null' in JSON. + public override bool HandleNull => true; + + private readonly JsonConverter _elementConverter; + private readonly FSharpCoreReflectionProxy.StructGetter _optionValueGetter; + private readonly Func _optionConstructor; + private readonly ConverterStrategy _converterStrategy; + + [RequiresUnreferencedCode(FSharpCoreReflectionProxy.FSharpCoreUnreferencedCodeMessage)] + public FSharpValueOptionConverter(JsonConverter elementConverter) + { + _elementConverter = elementConverter; + _optionValueGetter = FSharpCoreReflectionProxy.Instance.CreateFSharpValueOptionValueGetter(); + _optionConstructor = FSharpCoreReflectionProxy.Instance.CreateFSharpValueOptionSomeConstructor(); + + // 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 TValueOption value) + { + // `null` values deserialize as `ValueNone` + if (!state.IsContinuation && reader.TokenType == JsonTokenType.Null) + { + value = default; + 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 = default; + return false; + } + + internal override bool OnTryWrite(Utf8JsonWriter writer, TValueOption value, JsonSerializerOptions options, ref WriteStack state) + { + if (value.Equals(default)) + { + // Write `ValueNone` values as null + writer.WriteNullValue(); + return true; + } + + TElement element = _optionValueGetter(ref 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, TValueOption value, JsonSerializerOptions options) + { + if (value.Equals(default)) + { + // Write `ValueNone` values as null + writer.WriteNullValue(); + } + else + { + TElement element = _optionValueGetter(ref value); + _elementConverter.Write(writer, element, options); + } + } + + public override TValueOption Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return default; + } + + TElement? element = _elementConverter.Read(ref reader, typeToConvert, options); + return _optionConstructor(element); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverterFactory.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverterFactory.cs index 1a3903a5327b0e..aee6ff86db31fc 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverterFactory.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverterFactory.cs @@ -15,8 +15,14 @@ namespace System.Text.Json.Serialization.Converters /// internal sealed class ObjectConverterFactory : JsonConverterFactory { + // Need to toggle this behavior when generating converters for F# struct records. + private readonly bool _useDefaultConstructorInUnannotatedStructs; + [RequiresUnreferencedCode(JsonSerializer.SerializationUnreferencedCodeMessage)] - public ObjectConverterFactory() { } + public ObjectConverterFactory(bool useDefaultConstructorInUnannotatedStructs = true) + { + _useDefaultConstructorInUnannotatedStructs = useDefaultConstructorInUnannotatedStructs; + } public override bool CanConvert(Type typeToConvert) { @@ -164,7 +170,7 @@ private JsonConverter CreateKeyValuePairConverter(Type type, JsonSerializerOptio } // Structs will use default constructor if attribute isn't used. - if (type.IsValueType && ctorWithAttribute == null) + if (_useDefaultConstructorInUnannotatedStructs && type.IsValueType && ctorWithAttribute == null) { return null; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs index 33f45fd3ceda5f..3a83812d3396e6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverterOfT.cs @@ -545,9 +545,17 @@ internal void VerifyRead(JsonTokenType tokenType, int depth, long bytesConsumed, break; default: - // A non-value converter (object or collection) should always have Start and End tokens. + // A non-value converter (object or collection) should always have Start and End tokens + if (!isValueConverter) + { + // with the exception of converters that support null value reads + if (!HandleNullOnRead || tokenType != JsonTokenType.Null) + { + ThrowHelper.ThrowJsonException_SerializationConverterRead(this); + } + } // A value converter should not make any reads. - if (!isValueConverter || reader.BytesConsumed != bytesConsumed) + else if (reader.BytesConsumed != bytesConsumed) { ThrowHelper.ThrowJsonException_SerializationConverterRead(this); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs index a5270028def60d..27bcc4fb961c1f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs @@ -38,6 +38,7 @@ private void RootBuiltInConverters() new NullableConverterFactory(), new EnumConverterFactory(), new JsonNodeConverterFactory(), + new FSharpTypeConverterFactory(), // IAsyncEnumerable takes precedence over IEnumerable. new IAsyncEnumerableConverterFactory(), // IEnumerable should always be second to last since they can convert any IEnumerable. diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs new file mode 100644 index 00000000000000..a201c4b2fd9712 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/FSharpCoreReflectionProxy.cs @@ -0,0 +1,248 @@ +// 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; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace System.Text.Json.Serialization.Metadata +{ + // Recognizing types emitted by the F# compiler requires consuming APIs from the FSharp.Core runtime library. + // Every F# application ships with a copy of FSharp.Core, however it is not available statically to System.Text.Json. + // The following class uses reflection to access the relevant APIs required to detect the various F# types we are looking to support. + + /// + /// Proxy class used to access FSharp.Core metadata and reflection APIs that are not statically available to System.Text.Json. + /// + internal sealed class FSharpCoreReflectionProxy + { + /// + /// The various categories of F# types that System.Text.Json supports. + /// + public enum FSharpKind + { + Unrecognized, + Option, + ValueOption, + List, + Set, + Map, + Record, + Union + } + + // Binding a struct getter method to a delegate requires that the struct parameter is passed byref. + public delegate TResult StructGetter(ref TStruct @this) where TStruct : struct; + + public const string FSharpCoreUnreferencedCodeMessage = "Uses Reflection to access FSharp.Core components at runtime."; + + private static FSharpCoreReflectionProxy? s_singletonInstance; + + // Every type generated by the F# compiler is annotated with the CompilationMappingAttribute + // containing all relevant metadata required to determine its kind: + // https://fsharp.github.io/fsharp-core-docs/reference/fsharp-core-compilationmappingattribute.html#SourceConstructFlags + private const string CompilationMappingAttributeTypeName = "Microsoft.FSharp.Core.CompilationMappingAttribute"; + private readonly Type _compilationMappingAttributeType; + private readonly MethodInfo? _sourceConstructFlagsGetter; + + private readonly Type? _fsharpOptionType; + private readonly Type? _fsharpValueOptionType; + private readonly Type? _fsharpListType; + private readonly Type? _fsharpSetType; + private readonly Type? _fsharpMapType; + + private readonly MethodInfo? _fsharpListCtor; + private readonly MethodInfo? _fsharpSetCtor; + private readonly MethodInfo? _fsharpMapCtor; + + /// + /// Checks if the provided System.Type instance is emitted by the F# compiler. + /// If true, also initializes the proxy singleton for future by other F# types. + /// + [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] + public static bool IsFSharpType(Type type) + { + if (s_singletonInstance is null) + { + if (GetFSharpCoreAssembly(type) is Assembly fsharpCoreAssembly) + { + // Type is F# type, initialize the singleton instance. + s_singletonInstance ??= new FSharpCoreReflectionProxy(fsharpCoreAssembly); + + return true; + } + + return false; + } + + return s_singletonInstance.GetFSharpCompilationMappingAttribute(type) is not null; + } + + /// + /// Gets the singleton proxy instance; prerequires a successful IsFSharpType call for proxy initialization. + /// + public static FSharpCoreReflectionProxy Instance + { + get + { + Debug.Assert(s_singletonInstance is not null, "should be initialized via a successful IsFSharpType call."); + return s_singletonInstance; + } + } + + [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] + private FSharpCoreReflectionProxy(Assembly fsharpCoreAssembly) + { + Debug.Assert(fsharpCoreAssembly.GetName().Name == "FSharp.Core"); + + Type compilationMappingAttributeType = fsharpCoreAssembly.GetType(CompilationMappingAttributeTypeName)!; + _sourceConstructFlagsGetter = compilationMappingAttributeType.GetMethod("get_SourceConstructFlags", BindingFlags.Public | BindingFlags.Instance); + _compilationMappingAttributeType = compilationMappingAttributeType; + + _fsharpOptionType = fsharpCoreAssembly.GetType("Microsoft.FSharp.Core.FSharpOption`1"); + _fsharpValueOptionType = fsharpCoreAssembly.GetType("Microsoft.FSharp.Core.FSharpValueOption`1"); + _fsharpListType = fsharpCoreAssembly.GetType("Microsoft.FSharp.Collections.FSharpList`1"); + _fsharpSetType = fsharpCoreAssembly.GetType("Microsoft.FSharp.Collections.FSharpSet`1"); + _fsharpMapType = fsharpCoreAssembly.GetType("Microsoft.FSharp.Collections.FSharpMap`2"); + + _fsharpListCtor = fsharpCoreAssembly.GetType("Microsoft.FSharp.Collections.ListModule")?.GetMethod("OfSeq", BindingFlags.Public | BindingFlags.Static); + _fsharpSetCtor = fsharpCoreAssembly.GetType("Microsoft.FSharp.Collections.SetModule")?.GetMethod("OfSeq", BindingFlags.Public | BindingFlags.Static); + _fsharpMapCtor = fsharpCoreAssembly.GetType("Microsoft.FSharp.Collections.MapModule")?.GetMethod("OfSeq", BindingFlags.Public | BindingFlags.Static); + } + + [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] + public FSharpKind DetectFSharpKind(Type type) + { + Attribute? compilationMappingAttribute = GetFSharpCompilationMappingAttribute(type); + + if (compilationMappingAttribute is null) + { + return FSharpKind.Unrecognized; + } + + if (type.IsGenericType) + { + Type genericType = type.GetGenericTypeDefinition(); + if (genericType == _fsharpOptionType) return FSharpKind.Option; + if (genericType == _fsharpValueOptionType) return FSharpKind.ValueOption; + if (genericType == _fsharpListType) return FSharpKind.List; + if (genericType == _fsharpSetType) return FSharpKind.Set; + if (genericType == _fsharpMapType) return FSharpKind.Map; + } + + return (GetSourceConstructFlags(compilationMappingAttribute) & SourceConstructFlags.KindMask) switch + { + SourceConstructFlags.RecordType => FSharpKind.Record, + SourceConstructFlags.SumType => FSharpKind.Union, + _ => FSharpKind.Unrecognized + }; + } + + [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] + public Func CreateFSharpOptionValueGetter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TFSharpOption, T>() + { + Debug.Assert(typeof(TFSharpOption).GetGenericTypeDefinition() == _fsharpOptionType); + MethodInfo valueGetter = EnsureMemberExists(typeof(TFSharpOption).GetMethod("get_Value", BindingFlags.Public | BindingFlags.Instance), "Microsoft.FSharp.Core.FSharpOption.get_Value()"); + return CreateDelegate>(valueGetter); + } + + [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] + public Func CreateFSharpOptionSomeConstructor<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TFSharpOption, TElement>() + { + Debug.Assert(typeof(TFSharpOption).GetGenericTypeDefinition() == _fsharpOptionType); + MethodInfo methodInfo = EnsureMemberExists(typeof(TFSharpOption).GetMethod("Some", BindingFlags.Public | BindingFlags.Static), "Microsoft.FSharp.Core.FSharpOption.Some(T value)"); + return CreateDelegate>(methodInfo); + } + + [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] + public StructGetter CreateFSharpValueOptionValueGetter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TFSharpValueOption, TElement>() + where TFSharpValueOption : struct + { + Debug.Assert(typeof(TFSharpValueOption).GetGenericTypeDefinition() == _fsharpValueOptionType); + MethodInfo valueGetter = EnsureMemberExists(typeof(TFSharpValueOption).GetMethod("get_Value", BindingFlags.Public | BindingFlags.Instance), "Microsoft.FSharp.Core.FSharpValueOption.get_Value()"); + return CreateDelegate>(valueGetter); + } + + [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] + public Func CreateFSharpValueOptionSomeConstructor<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TFSharpOption, TElement>() + { + Debug.Assert(typeof(TFSharpOption).GetGenericTypeDefinition() == _fsharpValueOptionType); + MethodInfo methodInfo = EnsureMemberExists(typeof(TFSharpOption).GetMethod("Some", BindingFlags.Public | BindingFlags.Static), "Microsoft.FSharp.Core.FSharpValueOption.ValueSome(T value)"); + return CreateDelegate>(methodInfo); + } + + [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] + public Func, TFSharpList> CreateFSharpListConstructor() + { + Debug.Assert(typeof(TFSharpList).GetGenericTypeDefinition() == _fsharpListType); + return CreateDelegate, TFSharpList>>(EnsureMemberExists(_fsharpListCtor, "Microsoft.FSharp.Collections.ListModule.OfSeq(IEnumerable source)").MakeGenericMethod(typeof(TElement))); + } + + [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] + public Func, TFSharpSet> CreateFSharpSetConstructor() + { + Debug.Assert(typeof(TFSharpSet).GetGenericTypeDefinition() == _fsharpSetType); + return CreateDelegate, TFSharpSet>>(EnsureMemberExists(_fsharpSetCtor, "Microsoft.FSharp.Collections.SetModule.OfSeq(IEnumerable source)").MakeGenericMethod(typeof(TElement))); + } + + [RequiresUnreferencedCode(FSharpCoreUnreferencedCodeMessage)] + public Func>, TFSharpMap> CreateFSharpMapConstructor() + { + Debug.Assert(typeof(TFSharpMap).GetGenericTypeDefinition() == _fsharpMapType); + return CreateDelegate>, TFSharpMap>>(EnsureMemberExists(_fsharpMapCtor, "Microsoft.FSharp.Collections.MapModule.OfSeq(IEnumerable> source)").MakeGenericMethod(typeof(TKey), typeof(TValue))); + } + + private Attribute? GetFSharpCompilationMappingAttribute(Type type) + => type.GetCustomAttribute(_compilationMappingAttributeType, inherit: true); + + private SourceConstructFlags GetSourceConstructFlags(Attribute compilationMappingAttribute) + => _sourceConstructFlagsGetter is null ? SourceConstructFlags.None : (SourceConstructFlags)_sourceConstructFlagsGetter.Invoke(compilationMappingAttribute, null)!; + + // If the provided type is generated by the F# compiler, returns the runtime FSharp.Core assembly. + private static Assembly? GetFSharpCoreAssembly(Type type) + { + foreach (Attribute attr in type.GetCustomAttributes(inherit: true)) + { + Type attributeType = attr.GetType(); + if (attributeType.FullName == CompilationMappingAttributeTypeName) + { + return attributeType.Assembly; + } + } + + return null; + } + + private static TDelegate CreateDelegate(MethodInfo methodInfo) where TDelegate : Delegate + => (TDelegate)Delegate.CreateDelegate(typeof(TDelegate), methodInfo, throwOnBindFailure: true)!; + + private TMemberInfo EnsureMemberExists(TMemberInfo? memberInfo, string memberName) where TMemberInfo : MemberInfo + { + if (memberInfo is null) + { + ThrowHelper.ThrowMissingMemberException_MissingFSharpCoreMember(memberName); + } + + return memberInfo; + } + + // Replicates the F# source construct flags enum + // https://fsharp.github.io/fsharp-core-docs/reference/fsharp-core-sourceconstructflags.html + private enum SourceConstructFlags + { + None = 0, + SumType = 1, + RecordType = 2, + ObjectType = 3, + Field = 4, + Exception = 5, + Closure = 6, + Module = 7, + UnionCase = 8, + Value = 9, + KindMask = 31, + NonPublicRepresentation = 32 + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs index dda0e7f12dd1fe..28d244109f0d4e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs @@ -296,9 +296,6 @@ value is not null && if (Converter.HandleNullOnWrite) { - // No object, collection, or re-entrancy converter handles null. - Debug.Assert(Converter.ConverterStrategy == ConverterStrategy.Value); - if (state.Current.PropertyState < StackFramePropertyState.Name) { state.Current.PropertyState = StackFramePropertyState.Name; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs index 077de6d05372dc..956fefeee476c6 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs @@ -49,9 +49,6 @@ internal JsonTypeInfo? ElementTypeInfo { if (_elementTypeInfo == null && ElementType != null) { - Debug.Assert(PropertyInfoForTypeInfo.ConverterStrategy == ConverterStrategy.Enumerable || - PropertyInfoForTypeInfo.ConverterStrategy == ConverterStrategy.Dictionary); - _elementTypeInfo = Options.GetOrAddClass(ElementType); } @@ -181,6 +178,8 @@ internal JsonTypeInfo(Type type, JsonConverter converter, Type runtimeType, Json PropertyInfoForTypeInfo = CreatePropertyInfoForTypeInfo(Type, runtimeType, converter, typeNumberHandling, Options); + ElementType = converter.ElementType; + switch (PropertyInfoForTypeInfo.ConverterStrategy) { case ConverterStrategy.Object: @@ -303,14 +302,12 @@ internal JsonTypeInfo(Type type, JsonConverter converter, Type runtimeType, Json break; case ConverterStrategy.Enumerable: { - ElementType = converter.ElementType; CreateObject = Options.MemberAccessorStrategy.CreateConstructor(runtimeType); } break; case ConverterStrategy.Dictionary: { KeyType = converter.KeyType; - ElementType = converter.ElementType; CreateObject = Options.MemberAccessorStrategy.CreateConstructor(runtimeType); } break; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs index 61d953ac94ce2e..b8df6c1e10d266 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs @@ -716,5 +716,12 @@ public static void ThrowInvalidOperationException_NoDefaultOptionsForContext(Jso { throw new InvalidOperationException(SR.Format(SR.NoDefaultOptionsForContext, context.GetType(), type)); } + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + public static void ThrowMissingMemberException_MissingFSharpCoreMember(string missingFsharpCoreMember) + { + throw new MissingMemberException(SR.Format(SR.MissingFSharpCoreMember, missingFsharpCoreMember)); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/CollectionTests.fs b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/CollectionTests.fs new file mode 100644 index 00000000000000..af4326d9ccb4bb --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/CollectionTests.fs @@ -0,0 +1,177 @@ +module System.Text.Json.Tests.FSharp.CollectionTests + +open System.Text.Json +open System.Text.Json.Tests.FSharp.Helpers +open Xunit + +let getListsAndSerializations() = seq { + let wrapArgs (list : 'T list) (json : string) = [| box list ; box json |] + + wrapArgs [] "[]" + wrapArgs [1] "[1]" + wrapArgs [1;2;1;2;3;2;2;1;3;3;3] "[1,2,1,2,3,2,2,1,3,3,3]" + wrapArgs [false;true] "[false,true]" + wrapArgs [3.14] "[3.14]" + wrapArgs ["apple";"banana";"cherry"] """["apple","banana","cherry"]""" + wrapArgs [{| x = 0 ; y = 1 |}] """[{"x":0,"y":1}]""" + wrapArgs Unchecked.defaultof "null" // we support null list serialization and deserialization +} + +[] +[] +let ``Lists should have expected serialization`` (list : 'T list) (expectedJson : string) = + let actualJson = JsonSerializer.Serialize list + Assert.Equal(expectedJson, actualJson) + +[] +[] +let ``Lists should have expected deserialization``(expectedList : 'T list) (json : string) = + let actualList = JsonSerializer.Deserialize<'T list> json + Assert.Equal<'T>(expectedList, actualList) + +[] +[] +[] +[] +[] +[] +let ``List deserialization should reject invalid inputs``(json : string) = + Assert.Throws(fun () -> JsonSerializer.Deserialize(json) |> ignore) |> ignore + +[] +let ``List async serialization should be supported``() = async { + let inputs = [1 .. 200] + let expectedJson = JsonSerializer.Serialize inputs + + let options = new JsonSerializerOptions(DefaultBufferSize = 1) + let! actualJson = JsonSerializer.SerializeAsync(inputs, options) + + Assert.Equal(expectedJson, actualJson) +} + +[] +let ``List async deserialization should be supported``() = async { + let inputs = [1 .. 200] + let json = JsonSerializer.Serialize inputs + + let options = new JsonSerializerOptions(DefaultBufferSize = 1) + let! result = JsonSerializer.DeserializeAsync(json, options) + + Assert.Equal(inputs, result) +} + +let getSetsAndSerializations() = seq { + let wrapArgs (set : Set<'T>) (json : string) = [| box set ; box json |] + + wrapArgs Set.empty "[]" + wrapArgs (set [1]) "[1]" + wrapArgs (set [1;2;3]) "[1,2,3]" + wrapArgs (set [false;true]) "[false,true]" + wrapArgs (set [3.14]) "[3.14]" + wrapArgs (set ["apple";"banana";"cherry"]) """["apple","banana","cherry"]""" + wrapArgs (set [{| x = 0 ; y = 1 |}]) """[{"x":0,"y":1}]""" + wrapArgs Unchecked.defaultof> "null" // we support null set serialization and deserialization +} + +[] +[] +let ``Sets should have expected serialization`` (set : Set<'T>) (expectedJson : string) = + let actualJson = JsonSerializer.Serialize set + Assert.Equal(expectedJson, actualJson) + +[] +[] +let ``Sets should have expected deserialization`` (expectedSet : Set<'T>) (json : string) = + let actualSet = JsonSerializer.Deserialize> json + Assert.Equal>(expectedSet, actualSet) + +[] +let ``Set deserialization should trim duplicate elements`` () = + let expectedSet = set [1;2;3] + let actualSet = JsonSerializer.Deserialize> "[1,2,1,2,3,2,2,1,3,3,3]" + Assert.Equal>(expectedSet, actualSet) + +[] +[] +[] +[] +[] +[] +let ``Set deserialization should reject invalid inputs``(json : string) = + Assert.Throws(fun () -> JsonSerializer.Deserialize>(json) |> ignore) |> ignore + +[] +let ``Set async serialization should be supported``() = async { + let inputs = set [1 .. 200] + let expectedJson = JsonSerializer.Serialize inputs + + let options = new JsonSerializerOptions(DefaultBufferSize = 1) + let! actualJson = JsonSerializer.SerializeAsync(inputs, options) + + Assert.Equal(expectedJson, actualJson) +} + +[] +let ``Set async deserialization should be supported``() = async { + let inputs = set [1 .. 200] + let json = JsonSerializer.Serialize inputs + + let options = new JsonSerializerOptions(DefaultBufferSize = 1) + let! result = JsonSerializer.DeserializeAsync>(json, options) + + Assert.Equal>(inputs, result) +} + +let getMapsAndSerializations() = seq { + let wrapArgs (set : Map<'K,'V>) (json : string) = [| box set ; box json |] + + wrapArgs Map.empty "{}" + wrapArgs (Map.ofList [("key", "value")]) """{"key":"value"}""" + wrapArgs (Map.ofList [(1, 1); (2, 1)]) """{"1":1,"2":1}""" + wrapArgs (Map.ofList [(false, 1); (true, 1)]) """{"False":1,"True":1}""" + wrapArgs (Map.ofList [("fruit", ["apple";"banana";"cherry"])]) """{"fruit":["apple","banana","cherry"]}""" + wrapArgs (Map.ofList [("coordinates", {| x = 0 ; y = 1 |})]) """{"coordinates":{"x":0,"y":1}}""" + wrapArgs Unchecked.defaultof> "null" // we support null set serialization and deserialization +} + +[] +[] +let ``Maps should have expected serialization`` (map : Map<'key, 'value>) (expectedJson : string) = + let actualJson = JsonSerializer.Serialize map + Assert.Equal(expectedJson, actualJson) + +[] +[] +let ``Maps should have expected deserialization`` (expectedMap : Map<'key, 'value>) (json : string) = + let actualMap = JsonSerializer.Deserialize> json + Assert.Equal>(expectedMap, actualMap) + +[] +[] +[] +[] +[] +let ``Map deserialization should reject invalid inputs``(json : string) = + Assert.Throws(fun () -> JsonSerializer.Deserialize>(json) |> ignore) |> ignore + +[] +let ``Map async serialization should be supported``() = async { + let inputs = Map.ofList [for i in 1 .. 200 -> (i.ToString(), i)] + let expectedJson = JsonSerializer.Serialize inputs + + let options = new JsonSerializerOptions(DefaultBufferSize = 1) + let! actualJson = JsonSerializer.SerializeAsync(inputs, options) + + Assert.Equal(expectedJson, actualJson) +} + +[] +let ``Map async deserialization should be supported``() = async { + let inputs = Map.ofList [for i in 1 .. 200 -> (i.ToString(), i)] + let json = JsonSerializer.Serialize inputs + + let options = new JsonSerializerOptions(DefaultBufferSize = 1) + let! result = JsonSerializer.DeserializeAsync>(json, options) + + Assert.Equal>(inputs, result) +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/Helpers.fs b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/Helpers.fs new file mode 100644 index 00000000000000..b2310053bebca5 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/Helpers.fs @@ -0,0 +1,19 @@ +module System.Text.Json.Tests.FSharp.Helpers + +open System.IO +open System.Text +open System.Text.Json + +type JsonSerializer with + static member SerializeAsync<'T>(value : 'T, options : JsonSerializerOptions) = async { + let! ct = Async.CancellationToken + use mem = new MemoryStream() + do! JsonSerializer.SerializeAsync(mem, value, options, ct) |> Async.AwaitTask + return Encoding.UTF8.GetString(mem.ToArray()) + } + + static member DeserializeAsync<'T>(json : string, options : JsonSerializerOptions) = async { + let! ct = Async.CancellationToken + use mem = new MemoryStream(Encoding.UTF8.GetBytes json) + return! JsonSerializer.DeserializeAsync<'T>(mem, options).AsTask() |> Async.AwaitTask + } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/OptionTests.fs b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/OptionTests.fs new file mode 100644 index 00000000000000..ba97af58af7d69 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/OptionTests.fs @@ -0,0 +1,166 @@ +module System.Text.Json.Tests.FSharp.OptionTests + +open System.Text.Json +open System.Text.Json.Serialization +open System.Text.Json.Tests.FSharp.Helpers +open Xunit + +let getOptionalElementInputs() = seq { + let wrap value = [| box value |] + + wrap 42 + wrap false + wrap "string" + wrap [|1..5|] + wrap (3,2) + wrap {| Name = "Mary" ; Age = 32 |} + wrap struct {| Name = "Mary" ; Age = 32 |} + wrap [false; true; false; false] + wrap (Set.ofSeq [1 .. 5]) + wrap (Map.ofSeq [("key1", "value1"); ("key2", "value2")]) +} + +[] +[] +let ``Root-level None should serialize as null``(_ : 'T) = + let expected = "null" + let actual = JsonSerializer.Serialize<'T option>(None) + Assert.Equal(expected, actual) + +[] +[] +let ``None property should serialize as null``(_ : 'T) = + let expected = """{"value":null}""" + let actual = JsonSerializer.Serialize {| value = Option<'T>.None |} + Assert.Equal(expected, actual) + +[] +[] +let ``None collection element should serialize as null``(_ : 'T) = + let expected = """[null]""" + let actual = JsonSerializer.Serialize [| Option<'T>.None |] + Assert.Equal(expected, actual) + +[] +[] +let ``Root-level Some should serialize as the payload`` (value : 'T) = + let expected = JsonSerializer.Serialize(value) + let actual = JsonSerializer.Serialize(Some value) + Assert.Equal(expected, actual) + +[] +[] +let ``Some property should serialize as the payload`` (value : 'T) = + let expected = JsonSerializer.Serialize {| value = value |} + let actual = JsonSerializer.Serialize {| value = Some value |} + Assert.Equal(expected, actual) + +[] +[] +let ``Some collection element should serialize as the payload`` (value : 'T) = + let expected = JsonSerializer.Serialize [|value|] + let actual = JsonSerializer.Serialize [|Some value|] + Assert.Equal(expected, actual) + +[] +let ``Some of null should serialize as null`` () = + let expected = "null" + let actual = JsonSerializer.Serialize(Some null) + Assert.Equal(expected, actual) + +[] +[] +let ``Some of None should serialize as null`` (_ : 'T) = + let expected = "null" + let actual = JsonSerializer.Serialize<'T option option>(Some None) + Assert.Equal(expected, actual) + +[] +[] +let ``Some of Some of None should serialize as null`` (_ : 'T) = + let expected = "null" + let actual = JsonSerializer.Serialize<'T option option option>(Some (Some None)) + Assert.Equal(expected, actual) + +[] +[] +let ``Some of Some of value should serialize as value`` (value : 'T) = + let expected = JsonSerializer.Serialize value + let actual = JsonSerializer.Serialize(Some (Some value)) + Assert.Equal(expected, actual) + +[] +[] +let ``WhenWritingNull enabled should skip None properties``(_ : 'T) = + let expected = "{}" + let options = new JsonSerializerOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull) + let actual = JsonSerializer.Serialize<{| value : 'T option |}>({| value = None |}, options) + Assert.Equal(expected, actual) + +[] +[] +let ``Root-level null should deserialize as None``(_ : 'T) = + let actual = JsonSerializer.Deserialize<'T option>("null") + Assert.Equal(None, actual) + +[] +[] +let ``Null property should deserialize as None``(_ : 'T) = + let actual = JsonSerializer.Deserialize<{| value : 'T option |}>("""{"value":null}""") + Assert.Equal(None, actual.value) + +[] +[] +let ``Missing property should deserialize as None``(_ : 'T) = + let actual = JsonSerializer.Deserialize<{| value : 'T option |}>("{}") + Assert.Equal(None, actual.value) + +[] +[] +let ``Null element should deserialize as None``(_ : 'T) = + let expected = [Option<'T>.None] + let actual = JsonSerializer.Deserialize<'T option []>("""[null]""") + Assert.Equal(expected, actual) + +[] +[] +let ``Root-level value should deserialize as Some``(value : 'T) = + let json = JsonSerializer.Serialize(value) + let actual = JsonSerializer.Deserialize<'T option>(json) + Assert.Equal(Some value, actual) + +[] +[] +let ``Property value should deserialize as Some``(value : 'T) = + let json = JsonSerializer.Serialize {| value = value |} + let actual = JsonSerializer.Deserialize<{| value : 'T option|}>(json) + Assert.Equal(Some value, actual.value) + +[] +[] +let ``Collection element should deserialize as Some``(value : 'T) = + let json = JsonSerializer.Serialize [| value |] + let actual = JsonSerializer.Deserialize<'T option []>(json) + Assert.Equal([Some value], actual) + +[] +let ``Optional value should support resumable serialization``() = async { + let valueToSerialize = {| Values = Some [|1 .. 200|] |} + let expectedJson = JsonSerializer.Serialize valueToSerialize + + let options = new JsonSerializerOptions(DefaultBufferSize = 1) + let! actualJson = JsonSerializer.SerializeAsync(valueToSerialize, options) + + Assert.Equal(expectedJson, actualJson) +} + +[] +let ``Optional value should support resumable deserialization``() = async { + let valueToSerialize = {| Values = Some [|1 .. 200|] |} + let json = JsonSerializer.Serialize valueToSerialize + + let options = new JsonSerializerOptions(DefaultBufferSize = 1) + let! result = JsonSerializer.DeserializeAsync<{| Values : int [] option |}>(json, options) + + Assert.Equal(valueToSerialize, result) +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/RecordTests.fs b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/RecordTests.fs new file mode 100644 index 00000000000000..0d78940904b52c --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/RecordTests.fs @@ -0,0 +1,51 @@ +module System.Text.Json.Tests.FSharp.RecordTests + +open System.Text.Json +open System.Text.Json.Serialization +open System.Text.Json.Tests.FSharp.Helpers +open Xunit + +type MyRecord = + { + Name : string + MiddleName : string option + LastName : string + Age : int + IsActive : bool + } +with + static member Value = { Name = "John" ; MiddleName = None ; LastName = "Doe" ; Age = 34 ; IsActive = true } + static member ExpectedJson = """{"Name":"John","MiddleName":null,"LastName":"Doe","Age":34,"IsActive":true}""" + +[] +let ``Support F# record serialization``() = + let actualJson = JsonSerializer.Serialize(MyRecord.Value) + Assert.Equal(MyRecord.ExpectedJson, actualJson) + +[] +let ``Support F# record deserialization``() = + let result = JsonSerializer.Deserialize(MyRecord.ExpectedJson) + Assert.Equal(MyRecord.Value, result) + +[] +type MyStructRecord = + { + Name : string + MiddleName : string option + LastName : string + Age : int + IsActive : bool + } +with + static member Value = { Name = "John" ; MiddleName = None ; LastName = "Doe" ; Age = 34 ; IsActive = true } + static member ExpectedJson = """{"Name":"John","MiddleName":null,"LastName":"Doe","Age":34,"IsActive":true}""" + +[] +let ``Support F# struct record serialization``() = + let actualJson = JsonSerializer.Serialize(MyStructRecord.Value) + Assert.Equal(MyStructRecord.ExpectedJson, actualJson) + +[] +let ``Support F# struct record deserialization``() = + let result = JsonSerializer.Deserialize(MyStructRecord.ExpectedJson) + Assert.Equal(MyStructRecord.Value, result) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/System.Text.Json.FSharp.Tests.fsproj b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/System.Text.Json.FSharp.Tests.fsproj new file mode 100644 index 00000000000000..3fef790dcfec62 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/System.Text.Json.FSharp.Tests.fsproj @@ -0,0 +1,16 @@ + + + + $(NetCoreAppCurrent) + + + + + + + + + + + + diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/UnionTests.fs b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/UnionTests.fs new file mode 100644 index 00000000000000..d149ea6c27a7f1 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/UnionTests.fs @@ -0,0 +1,39 @@ +module System.Text.Json.Tests.FSharp.UnionTests + +open System +open System.Text.Json +open System.Text.Json.Serialization +open Xunit + +type MySingleCaseUnion = MySingleCaseUnion of string +type MyTypeSafeEnum = Label1 | Label2 | Label3 +type MyMultiCaseUnion = Point | Circle of radius:float | Rectangle of height:float * length:float + +[] +type MyStructSingleCaseUnion = MyStructSingleCaseUnion of string +[] +type MyStructTypeSafeEnum = StructLabel1 | StructLabel2 | StructLabel3 +[] +type MyStructMultiCaseUnion = StructPoint | StructCircle of radius:float | StructRectangle of height:float * length:float + +let getUnionValues() = seq { + let wrap value = [| value :> obj |] + + MySingleCaseUnion "value" |> wrap + Label1 |> wrap + Circle 1. |> wrap + + MyStructSingleCaseUnion "value" |> wrap + StructLabel2 |> wrap + StructCircle 1. |> wrap +} + +[] +[] +let ``Union serialization should throw NotSupportedException`` (value : 'T) = + Assert.Throws(fun () -> JsonSerializer.Serialize(value) |> ignore) + +[] +[] +let ``Union deserialization should throw NotSupportedException`` (value : 'T) = + Assert.Throws(fun () -> JsonSerializer.Deserialize<'T>("{}") |> ignore) diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/ValueOptionTests.fs b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/ValueOptionTests.fs new file mode 100644 index 00000000000000..666ad64b8ecaa0 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.FSharp.Tests/ValueOptionTests.fs @@ -0,0 +1,166 @@ +module System.Text.Json.Tests.FSharp.ValueOptionTests + +open System.Text.Json +open System.Text.Json.Serialization +open System.Text.Json.Tests.FSharp.Helpers +open Xunit + +let getOptionalElementInputs() = seq { + let wrap value = [| box value |] + + wrap 42 + wrap false + wrap "string" + wrap [|1..5|] + wrap (3,2) + wrap {| Name = "Mary" ; Age = 32 |} + wrap struct {| Name = "Mary" ; Age = 32 |} + wrap [false; true; false; false] + wrap (Set.ofSeq [1 .. 5]) + wrap (Map.ofSeq [("key1", "value1"); ("key2", "value2")]) +} + +[] +[] +let ``Root-level ValueNone should serialize as null``(_ : 'T) = + let expected = "null" + let actual = JsonSerializer.Serialize<'T voption>(ValueNone) + Assert.Equal(expected, actual) + +[] +[] +let ``ValueNone property should serialize as null``(_ : 'T) = + let expected = """{"value":null}""" + let actual = JsonSerializer.Serialize {| value = ValueOption<'T>.ValueNone |} + Assert.Equal(expected, actual) + +[] +[] +let ``ValueNone collection element should serialize as null``(_ : 'T) = + let expected = """[null]""" + let actual = JsonSerializer.Serialize [| ValueOption<'T>.ValueNone |] + Assert.Equal(expected, actual) + +[] +[] +let ``Root-level ValueSome should serialize as the payload`` (value : 'T) = + let expected = JsonSerializer.Serialize(value) + let actual = JsonSerializer.Serialize(ValueSome value) + Assert.Equal(expected, actual) + +[] +[] +let ``ValueSome property should serialize as the payload`` (value : 'T) = + let expected = JsonSerializer.Serialize {| value = value |} + let actual = JsonSerializer.Serialize {| value = ValueSome value |} + Assert.Equal(expected, actual) + +[] +[] +let ``ValueSome collection element should serialize as the payload`` (value : 'T) = + let expected = JsonSerializer.Serialize [|value|] + let actual = JsonSerializer.Serialize [|ValueSome value|] + Assert.Equal(expected, actual) + +[] +let ``ValueSome of null should serialize as null`` () = + let expected = "null" + let actual = JsonSerializer.Serialize(ValueSome null) + Assert.Equal(expected, actual) + +[] +[] +let ``ValueSome of ValueNone should serialize as null`` (_ : 'T) = + let expected = "null" + let actual = JsonSerializer.Serialize<'T voption voption>(ValueSome ValueNone) + Assert.Equal(expected, actual) + +[] +[] +let ``ValueSome of ValueSome of ValueNone should serialize as null`` (_ : 'T) = + let expected = "null" + let actual = JsonSerializer.Serialize<'T voption voption voption>(ValueSome (ValueSome ValueNone)) + Assert.Equal(expected, actual) + +[] +[] +let ``ValueSome of ValueSome of value should serialize as value`` (value : 'T) = + let expected = JsonSerializer.Serialize value + let actual = JsonSerializer.Serialize(ValueSome (ValueSome value)) + Assert.Equal(expected, actual) + +[] +[] +let ``WhenWritingDefault enabled should skip ValueNone properties``(_ : 'T) = + let expected = "{}" + let options = new JsonSerializerOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingDefault) + let actual = JsonSerializer.Serialize<{| value : 'T voption |}>({| value = ValueNone |}, options) + Assert.Equal(expected, actual) + +[] +[] +let ``Root-level null should deserialize as ValueNone``(_ : 'T) = + let actual = JsonSerializer.Deserialize<'T voption>("null") + Assert.Equal(ValueNone, actual) + +[] +[] +let ``Null property should deserialize as ValueNone``(_ : 'T) = + let actual = JsonSerializer.Deserialize<{| value : 'T voption |}>("""{"value":null}""") + Assert.Equal(ValueNone, actual.value) + +[] +[] +let ``Missing property should deserialize as ValueNone``(_ : 'T) = + let actual = JsonSerializer.Deserialize<{| value : 'T voption |}>("{}") + Assert.Equal(ValueNone, actual.value) + +[] +[] +let ``Null element should deserialize as ValueNone``(_ : 'T) = + let expected = [ValueOption<'T>.ValueNone] + let actual = JsonSerializer.Deserialize<'T voption []>("""[null]""") + Assert.Equal(expected, actual) + +[] +[] +let ``Root-level value should deserialize as ValueSome``(value : 'T) = + let json = JsonSerializer.Serialize(value) + let actual = JsonSerializer.Deserialize<'T voption>(json) + Assert.Equal(ValueSome value, actual) + +[] +[] +let ``Property value should deserialize as ValueSome``(value : 'T) = + let json = JsonSerializer.Serialize {| value = value |} + let actual = JsonSerializer.Deserialize<{| value : 'T voption|}>(json) + Assert.Equal(ValueSome value, actual.value) + +[] +[] +let ``Collection element should deserialize as ValueSome``(value : 'T) = + let json = JsonSerializer.Serialize [| value |] + let actual = JsonSerializer.Deserialize<'T voption []>(json) + Assert.Equal([ValueSome value], actual) + +[] +let ``Optional value should support resumable serialization``() = async { + let valueToSerialize = {| Values = ValueSome [|1 .. 200|] |} + let expectedJson = JsonSerializer.Serialize valueToSerialize + + let options = new JsonSerializerOptions(DefaultBufferSize = 1) + let! actualJson = JsonSerializer.SerializeAsync(valueToSerialize, options) + + Assert.Equal(expectedJson, actualJson) +} + +[] +let ``Optional value should support resumable deserialization``() = async { + let valueToSerialize = {| Values = ValueSome [|1 .. 200|] |} + let json = JsonSerializer.Serialize valueToSerialize + + let options = new JsonSerializerOptions(DefaultBufferSize = 1) + let! result = JsonSerializer.DeserializeAsync<{| Values : int [] voption |}>(json, options) + + Assert.Equal(valueToSerialize, result) +}