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