From 3be420042f922197441783e73adb51cca715a58c Mon Sep 17 00:00:00 2001 From: Daniel Little Dev Date: Fri, 21 Oct 2022 10:29:40 +1000 Subject: [PATCH] Add Support for Untagged Unwrapped Single Cases --- src/FSharp.SystemTextJson/Union.fs | 65 ++++++++++++++++++- .../FSharp.SystemTextJson.Tests/Test.Union.fs | 34 ++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) diff --git a/src/FSharp.SystemTextJson/Union.fs b/src/FSharp.SystemTextJson/Union.fs index f07a68c..8510bae 100644 --- a/src/FSharp.SystemTextJson/Union.fs +++ b/src/FSharp.SystemTextJson/Union.fs @@ -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() + 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.IsAssignableFrom(clrType) -> + dict[JsonTokenType.StartArray] <- c + | TypeCode.Object -> + dict[JsonTokenType.StartObject] <- c + | _ -> + () + ValueSome dict + else + ValueNone + let getJsonName (reader: byref) = match reader.TokenType with | JsonTokenType.True -> JsonName.Bool true @@ -533,6 +568,24 @@ type JsonUnionConverter<'T> | ValueNone -> failExpecting "case field" &reader ty | _ -> failExpecting "case field" &reader ty + let getCaseByElementType (reader: byref) = + 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) = + 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 @@ -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 @@ -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) = diff --git a/tests/FSharp.SystemTextJson.Tests/Test.Union.fs b/tests/FSharp.SystemTextJson.Tests/Test.Union.fs index 3fdd764..4f32fd1 100644 --- a/tests/FSharp.SystemTextJson.Tests/Test.Union.fs +++ b/tests/FSharp.SystemTextJson.Tests/Test.Union.fs @@ -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 + [] + 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)) + + [] + let ``deserialize untagged unwrapped single-field cases`` () = + let choice1 = JsonSerializer.Deserialize("1", untaggedUnwrappedSingleFieldCasesOptions) + Assert.Equal(Choice1Of5 1, choice1) + + let choice2 = JsonSerializer.Deserialize("\"F#\"", untaggedUnwrappedSingleFieldCasesOptions) + let expected2: ChoiceOf5 = Choice2Of5 "F#" + Assert.Equal(expected2, choice2) + + let choice3 = JsonSerializer.Deserialize("false", untaggedUnwrappedSingleFieldCasesOptions) + Assert.Equal(Choice3Of5 false, choice3) + + let choice4 = JsonSerializer.Deserialize("[1,2]", untaggedUnwrappedSingleFieldCasesOptions) + Assert.Equal(Choice4Of5 [1;2], choice4) + + let choice5 = JsonSerializer.Deserialize("""{"name":"Object"}""", untaggedUnwrappedSingleFieldCasesOptions) + Assert.Equal(Choice5Of5 { name = "Object" }, choice5) + let unwrapFieldlessTagsOptions = JsonSerializerOptions() unwrapFieldlessTagsOptions.Converters.Add(JsonFSharpConverter(JsonUnionEncoding.UnwrapFieldlessTags))