Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 59 additions & 3 deletions src/FSharp.SystemTextJson/Union.fs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,38 @@ type JsonUnionConverter<'T>
else
ValueNone

let casesByJsonType =
if fsOptions.UnionEncoding.HasFlag JsonUnionEncoding.Untagged
&& fsOptions.UnionEncoding.HasFlag JsonUnionEncoding.UnwrapSingleFieldCases then
let dict = Dictionary<JsonTokenType, Case>()
for c in cases do
let clrType = c.Fields[0].Type
let typeCode = Type.GetTypeCode(c.Fields[0].Type)
match typeCode with
| TypeCode.Byte
| TypeCode.SByte
| TypeCode.UInt16
| TypeCode.UInt32
| TypeCode.UInt64
| TypeCode.Int16
| TypeCode.Int32
| TypeCode.Int64
| TypeCode.Decimal
| TypeCode.Double
| TypeCode.Single -> dict[JsonTokenType.Number] <- c
| TypeCode.Boolean ->
dict[JsonTokenType.True] <- c
dict[JsonTokenType.False] <- c
| TypeCode.DateTime
| TypeCode.String -> dict[JsonTokenType.String] <- c
| TypeCode.Object when typeof<System.Collections.IEnumerable>.IsAssignableFrom (clrType) ->
dict[JsonTokenType.StartArray] <- c
| TypeCode.Object -> dict[JsonTokenType.StartObject] <- c
| _ -> ()
ValueSome dict
else
ValueNone

let getJsonName (reader: byref<Utf8JsonReader>) =
match reader.TokenType with
| JsonTokenType.True -> JsonName.Bool true
Expand Down Expand Up @@ -533,6 +565,24 @@ type JsonUnionConverter<'T>
| ValueNone -> failExpecting "case field" &reader ty
| _ -> failExpecting "case field" &reader ty

let getCaseByElementType (reader: byref<Utf8JsonReader>) =
let found =
match casesByJsonType with
| ValueNone -> ValueNone
| ValueSome d ->
match d.TryGetValue(reader.TokenType) with
| true, p -> ValueSome p
| false, _ -> ValueNone
match found with
| ValueNone ->
failf "Unknown case for union type %s due to unmatched field type: %s" ty.FullName (reader.GetString())
| ValueSome case -> case

let readUnwrapedUntagged (reader: byref<Utf8JsonReader>) =
let case = getCaseByElementType &reader
let field = JsonSerializer.Deserialize(&reader, case.Fields[0].Type, options)
case.Ctor [| field |] :?> 'T

let writeFieldsAsRestOfArray (writer: Utf8JsonWriter) (case: Case) (value: obj) (options: JsonSerializerOptions) =
let fields = case.Fields
let values = case.Dector value
Expand Down Expand Up @@ -614,7 +664,10 @@ type JsonUnionConverter<'T>
writeFieldsAsRestOfArray writer case value options

let writeUntagged (writer: Utf8JsonWriter) (case: Case) (value: obj) (options: JsonSerializerOptions) =
writeFieldsAsObject writer case value options
if case.UnwrappedSingleField then
JsonSerializer.Serialize(writer, (case.Dector value)[0], case.Fields[0].Type, options)
else
writeFieldsAsObject writer case value options

override _.Read(reader, _typeToConvert, options) =
match reader.TokenType with
Expand All @@ -633,11 +686,14 @@ type JsonUnionConverter<'T>
| JsonUnionEncoding.ExternalTag -> readExternalTag &reader options
| JsonUnionEncoding.InternalTag -> readInternalTag &reader options
| UntaggedBit ->
if not hasDistinctFieldNames then
if fsOptions.UnionEncoding.HasFlag JsonUnionEncoding.UnwrapSingleFieldCases then
readUnwrapedUntagged &reader
elif not hasDistinctFieldNames then
failf
"Union %s can't be deserialized as Untagged because it has duplicate field names across unions"
ty.FullName
readUntagged &reader options
else
readUntagged &reader options
| _ -> failf "Invalid union encoding: %A" fsOptions.UnionEncoding

override _.Write(writer, value, options) =
Expand Down
43 changes: 43 additions & 0 deletions tests/FSharp.SystemTextJson.Tests/Test.Union.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2017,6 +2017,49 @@ module Struct =
JsonSerializer.Serialize(Bc("test", true), unwrapSingleFieldCasesOptions)
)

let untaggedUnwrappedSingleFieldCasesOptions = JsonSerializerOptions()

untaggedUnwrappedSingleFieldCasesOptions.Converters.Add(
JsonFSharpConverter(JsonUnionEncoding.Untagged ||| JsonUnionEncoding.UnwrapSingleFieldCases)
)

type Object = { name: string }
type ChoiceOf5 = Choice<int, string, bool, int list, Object>

[<Fact>]
let ``serialize untagged unwrapped single-field cases`` () =
Assert.Equal("1", JsonSerializer.Serialize(Choice1Of5 1, untaggedUnwrappedSingleFieldCasesOptions))
Assert.Equal("\"F#\"", JsonSerializer.Serialize(Choice2Of5 "F#", untaggedUnwrappedSingleFieldCasesOptions))
Assert.Equal("false", JsonSerializer.Serialize(Choice3Of5 false, untaggedUnwrappedSingleFieldCasesOptions))
Assert.Equal("[1,2]", JsonSerializer.Serialize(Choice4Of5 [ 1; 2 ], untaggedUnwrappedSingleFieldCasesOptions))
Assert.Equal(
"{name:\"Object\"}",
JsonSerializer.Serialize(Choice5Of5 { name = "Object" }, untaggedUnwrappedSingleFieldCasesOptions)
)

[<Fact>]
let ``deserialize untagged unwrapped single-field cases`` () =
let choice1 =
JsonSerializer.Deserialize<ChoiceOf5>("1", untaggedUnwrappedSingleFieldCasesOptions)
Assert.Equal(Choice1Of5 1, choice1)

let choice2 =
JsonSerializer.Deserialize<ChoiceOf5>("\"F#\"", untaggedUnwrappedSingleFieldCasesOptions)
let expected2: ChoiceOf5 = Choice2Of5 "F#"
Assert.Equal(expected2, choice2)

let choice3 =
JsonSerializer.Deserialize<ChoiceOf5>("false", untaggedUnwrappedSingleFieldCasesOptions)
Assert.Equal(Choice3Of5 false, choice3)

let choice4 =
JsonSerializer.Deserialize<ChoiceOf5>("[1,2]", untaggedUnwrappedSingleFieldCasesOptions)
Assert.Equal(Choice4Of5 [ 1; 2 ], choice4)

let choice5 =
JsonSerializer.Deserialize<ChoiceOf5>("""{"name":"Object"}""", untaggedUnwrappedSingleFieldCasesOptions)
Assert.Equal(Choice5Of5 { name = "Object" }, choice5)

let unwrapFieldlessTagsOptions = JsonSerializerOptions()
unwrapFieldlessTagsOptions.Converters.Add(JsonFSharpConverter(JsonUnionEncoding.UnwrapFieldlessTags))

Expand Down