Skip to content

Commit 87efc5d

Browse files
Add Support for Untagged Unwrapped Single Cases
1 parent 1bda46b commit 87efc5d

File tree

2 files changed

+102
-3
lines changed

2 files changed

+102
-3
lines changed

src/FSharp.SystemTextJson/Union.fs

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

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

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

616666
let writeUntagged (writer: Utf8JsonWriter) (case: Case) (value: obj) (options: JsonSerializerOptions) =
617-
writeFieldsAsObject writer case value options
667+
if case.UnwrappedSingleField then
668+
JsonSerializer.Serialize(writer, (case.Dector value)[0], case.Fields[0].Type, options)
669+
else
670+
writeFieldsAsObject writer case value options
618671

619672
override _.Read(reader, _typeToConvert, options) =
620673
match reader.TokenType with
@@ -633,11 +686,14 @@ type JsonUnionConverter<'T>
633686
| JsonUnionEncoding.ExternalTag -> readExternalTag &reader options
634687
| JsonUnionEncoding.InternalTag -> readInternalTag &reader options
635688
| UntaggedBit ->
636-
if not hasDistinctFieldNames then
689+
if fsOptions.UnionEncoding.HasFlag JsonUnionEncoding.UnwrapSingleFieldCases then
690+
readUnwrapedUntagged &reader
691+
elif not hasDistinctFieldNames then
637692
failf
638693
"Union %s can't be deserialized as Untagged because it has duplicate field names across unions"
639694
ty.FullName
640-
readUntagged &reader options
695+
else
696+
readUntagged &reader options
641697
| _ -> failf "Invalid union encoding: %A" fsOptions.UnionEncoding
642698

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

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2017,6 +2017,49 @@ 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+
2029+
[<Fact>]
2030+
let ``serialize untagged unwrapped single-field cases`` () =
2031+
Assert.Equal("1", JsonSerializer.Serialize(Choice1Of5 1, untaggedUnwrappedSingleFieldCasesOptions))
2032+
Assert.Equal("\"F#\"", JsonSerializer.Serialize(Choice2Of5 "F#", untaggedUnwrappedSingleFieldCasesOptions))
2033+
Assert.Equal("false", JsonSerializer.Serialize(Choice3Of5 false, untaggedUnwrappedSingleFieldCasesOptions))
2034+
Assert.Equal("[1,2]", JsonSerializer.Serialize(Choice4Of5 [ 1; 2 ], untaggedUnwrappedSingleFieldCasesOptions))
2035+
Assert.Equal(
2036+
"{name:\"Object\"}",
2037+
JsonSerializer.Serialize(Choice5Of5 { name = "Object" }, untaggedUnwrappedSingleFieldCasesOptions)
2038+
)
2039+
2040+
[<Fact>]
2041+
let ``deserialize untagged unwrapped single-field cases`` () =
2042+
let choice1 =
2043+
JsonSerializer.Deserialize<ChoiceOf5>("1", untaggedUnwrappedSingleFieldCasesOptions)
2044+
Assert.Equal(Choice1Of5 1, choice1)
2045+
2046+
let choice2 =
2047+
JsonSerializer.Deserialize<ChoiceOf5>("\"F#\"", untaggedUnwrappedSingleFieldCasesOptions)
2048+
let expected2: ChoiceOf5 = Choice2Of5 "F#"
2049+
Assert.Equal(expected2, choice2)
2050+
2051+
let choice3 =
2052+
JsonSerializer.Deserialize<ChoiceOf5>("false", untaggedUnwrappedSingleFieldCasesOptions)
2053+
Assert.Equal(Choice3Of5 false, choice3)
2054+
2055+
let choice4 =
2056+
JsonSerializer.Deserialize<ChoiceOf5>("[1,2]", untaggedUnwrappedSingleFieldCasesOptions)
2057+
Assert.Equal(Choice4Of5 [ 1; 2 ], choice4)
2058+
2059+
let choice5 =
2060+
JsonSerializer.Deserialize<ChoiceOf5>("""{"name":"Object"}""", untaggedUnwrappedSingleFieldCasesOptions)
2061+
Assert.Equal(Choice5Of5 { name = "Object" }, choice5)
2062+
20202063
let unwrapFieldlessTagsOptions = JsonSerializerOptions()
20212064
unwrapFieldlessTagsOptions.Converters.Add(JsonFSharpConverter(JsonUnionEncoding.UnwrapFieldlessTags))
20222065

0 commit comments

Comments
 (0)