diff --git a/src/FSharp.SystemTextJson/Union.fs b/src/FSharp.SystemTextJson/Union.fs index f07a68c..867dc47 100644 --- a/src/FSharp.SystemTextJson/Union.fs +++ b/src/FSharp.SystemTextJson/Union.fs @@ -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() + 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 +565,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 +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 @@ -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) = diff --git a/tests/FSharp.SystemTextJson.Tests/Test.Union.fs b/tests/FSharp.SystemTextJson.Tests/Test.Union.fs index 3fdd764..86efad6 100644 --- a/tests/FSharp.SystemTextJson.Tests/Test.Union.fs +++ b/tests/FSharp.SystemTextJson.Tests/Test.Union.fs @@ -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 + + [] + 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))