Skip to content

Commit 3be4200

Browse files
Add Support for Untagged Unwrapped Single Cases
1 parent 1bda46b commit 3be4200

File tree

2 files changed

+96
-3
lines changed

2 files changed

+96
-3
lines changed

src/FSharp.SystemTextJson/Union.fs

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,41 @@ type JsonUnionConverter<'T>
210210
else
211211
ValueNone
212212

213+
let casesByJsonType =
214+
if fsOptions.UnionEncoding.HasFlag JsonUnionEncoding.Untagged && fsOptions.UnionEncoding.HasFlag JsonUnionEncoding.UnwrapSingleFieldCases then
215+
let dict = Dictionary<JsonTokenType, Case>()
216+
for c in cases do
217+
let clrType = c.Fields[0].Type
218+
let typeCode = Type.GetTypeCode(c.Fields[0].Type)
219+
match typeCode with
220+
| TypeCode.Byte
221+
| TypeCode.SByte
222+
| TypeCode.UInt16
223+
| TypeCode.UInt32
224+
| TypeCode.UInt64
225+
| TypeCode.Int16
226+
| TypeCode.Int32
227+
| TypeCode.Int64
228+
| TypeCode.Decimal
229+
| TypeCode.Double
230+
| TypeCode.Single ->
231+
dict[JsonTokenType.Number] <- c
232+
| TypeCode.Boolean ->
233+
dict[JsonTokenType.True] <- c
234+
dict[JsonTokenType.False] <- c
235+
| TypeCode.DateTime
236+
| TypeCode.String ->
237+
dict[JsonTokenType.String] <- c
238+
| TypeCode.Object when typeof<System.Collections.IEnumerable>.IsAssignableFrom(clrType) ->
239+
dict[JsonTokenType.StartArray] <- c
240+
| TypeCode.Object ->
241+
dict[JsonTokenType.StartObject] <- c
242+
| _ ->
243+
()
244+
ValueSome dict
245+
else
246+
ValueNone
247+
213248
let getJsonName (reader: byref<Utf8JsonReader>) =
214249
match reader.TokenType with
215250
| JsonTokenType.True -> JsonName.Bool true
@@ -533,6 +568,24 @@ type JsonUnionConverter<'T>
533568
| ValueNone -> failExpecting "case field" &reader ty
534569
| _ -> failExpecting "case field" &reader ty
535570

571+
let getCaseByElementType (reader: byref<Utf8JsonReader>) =
572+
let found =
573+
match casesByJsonType with
574+
| ValueNone ->
575+
ValueNone
576+
| ValueSome d ->
577+
match d.TryGetValue(reader.TokenType) with
578+
| true, p -> ValueSome p
579+
| false, _ -> ValueNone
580+
match found with
581+
| ValueNone -> failf "Unknown case for union type %s due to unmatched field type: %s" ty.FullName (reader.GetString())
582+
| ValueSome case -> case
583+
584+
let readUnwrapedUntagged (reader: byref<Utf8JsonReader>) =
585+
let case = getCaseByElementType &reader
586+
let field = JsonSerializer.Deserialize(&reader, case.Fields[0].Type, options)
587+
case.Ctor [| field |] :?> 'T
588+
536589
let writeFieldsAsRestOfArray (writer: Utf8JsonWriter) (case: Case) (value: obj) (options: JsonSerializerOptions) =
537590
let fields = case.Fields
538591
let values = case.Dector value
@@ -614,7 +667,10 @@ type JsonUnionConverter<'T>
614667
writeFieldsAsRestOfArray writer case value options
615668

616669
let writeUntagged (writer: Utf8JsonWriter) (case: Case) (value: obj) (options: JsonSerializerOptions) =
617-
writeFieldsAsObject writer case value options
670+
if case.UnwrappedSingleField then
671+
JsonSerializer.Serialize(writer, (case.Dector value)[0], case.Fields[0].Type, options)
672+
else
673+
writeFieldsAsObject writer case value options
618674

619675
override _.Read(reader, _typeToConvert, options) =
620676
match reader.TokenType with
@@ -633,11 +689,14 @@ type JsonUnionConverter<'T>
633689
| JsonUnionEncoding.ExternalTag -> readExternalTag &reader options
634690
| JsonUnionEncoding.InternalTag -> readInternalTag &reader options
635691
| UntaggedBit ->
636-
if not hasDistinctFieldNames then
692+
if fsOptions.UnionEncoding.HasFlag JsonUnionEncoding.UnwrapSingleFieldCases then
693+
readUnwrapedUntagged &reader
694+
elif not hasDistinctFieldNames then
637695
failf
638696
"Union %s can't be deserialized as Untagged because it has duplicate field names across unions"
639697
ty.FullName
640-
readUntagged &reader options
698+
else
699+
readUntagged &reader options
641700
| _ -> failf "Invalid union encoding: %A" fsOptions.UnionEncoding
642701

643702
override _.Write(writer, value, options) =

tests/FSharp.SystemTextJson.Tests/Test.Union.fs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2017,6 +2017,40 @@ module Struct =
20172017
JsonSerializer.Serialize(Bc("test", true), unwrapSingleFieldCasesOptions)
20182018
)
20192019

2020+
let untaggedUnwrappedSingleFieldCasesOptions = JsonSerializerOptions()
2021+
2022+
untaggedUnwrappedSingleFieldCasesOptions.Converters.Add(
2023+
JsonFSharpConverter(JsonUnionEncoding.Untagged ||| JsonUnionEncoding.UnwrapSingleFieldCases)
2024+
)
2025+
2026+
type Object = { name: string }
2027+
type ChoiceOf5 = Choice<int, string, bool, int list, Object>
2028+
[<Fact>]
2029+
let ``serialize untagged unwrapped single-field cases`` () =
2030+
Assert.Equal("1", JsonSerializer.Serialize(Choice1Of5 1, untaggedUnwrappedSingleFieldCasesOptions))
2031+
Assert.Equal("\"F#\"", JsonSerializer.Serialize(Choice2Of5 "F#", untaggedUnwrappedSingleFieldCasesOptions))
2032+
Assert.Equal("false", JsonSerializer.Serialize(Choice3Of5 false, untaggedUnwrappedSingleFieldCasesOptions))
2033+
Assert.Equal("[1,2]", JsonSerializer.Serialize(Choice4Of5 [1;2], untaggedUnwrappedSingleFieldCasesOptions))
2034+
Assert.Equal("{name:\"Object\"}", JsonSerializer.Serialize(Choice5Of5 { name = "Object" }, untaggedUnwrappedSingleFieldCasesOptions))
2035+
2036+
[<Fact>]
2037+
let ``deserialize untagged unwrapped single-field cases`` () =
2038+
let choice1 = JsonSerializer.Deserialize<ChoiceOf5>("1", untaggedUnwrappedSingleFieldCasesOptions)
2039+
Assert.Equal(Choice1Of5 1, choice1)
2040+
2041+
let choice2 = JsonSerializer.Deserialize<ChoiceOf5>("\"F#\"", untaggedUnwrappedSingleFieldCasesOptions)
2042+
let expected2: ChoiceOf5 = Choice2Of5 "F#"
2043+
Assert.Equal(expected2, choice2)
2044+
2045+
let choice3 = JsonSerializer.Deserialize<ChoiceOf5>("false", untaggedUnwrappedSingleFieldCasesOptions)
2046+
Assert.Equal(Choice3Of5 false, choice3)
2047+
2048+
let choice4 = JsonSerializer.Deserialize<ChoiceOf5>("[1,2]", untaggedUnwrappedSingleFieldCasesOptions)
2049+
Assert.Equal(Choice4Of5 [1;2], choice4)
2050+
2051+
let choice5 = JsonSerializer.Deserialize<ChoiceOf5>("""{"name":"Object"}""", untaggedUnwrappedSingleFieldCasesOptions)
2052+
Assert.Equal(Choice5Of5 { name = "Object" }, choice5)
2053+
20202054
let unwrapFieldlessTagsOptions = JsonSerializerOptions()
20212055
unwrapFieldlessTagsOptions.Converters.Add(JsonFSharpConverter(JsonUnionEncoding.UnwrapFieldlessTags))
20222056

0 commit comments

Comments
 (0)