Skip to content

Commit

Permalink
Add Support for Untagged Unwrapped Single Cases
Browse files Browse the repository at this point in the history
  • Loading branch information
daniellittledev committed Oct 21, 2022
1 parent 1bda46b commit 3be4200
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 3 deletions.
65 changes: 62 additions & 3 deletions src/FSharp.SystemTextJson/Union.fs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,41 @@ 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 +568,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 +667,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 +689,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
34 changes: 34 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,40 @@ 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

0 comments on commit 3be4200

Please sign in to comment.