Skip to content

Commit

Permalink
[#108] Add UnionFieldNamesFromTypes
Browse files Browse the repository at this point in the history
  • Loading branch information
Tarmil committed Jun 8, 2022
1 parent 2fc5b39 commit 596e4fc
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 4 deletions.
3 changes: 3 additions & 0 deletions src/FSharp.SystemTextJson/Options.fs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ type JsonUnionEncoding =
/// where the tag is not the first field in the JSON object.
| AllowUnorderedTag = 0x00_00_40_00

/// When a union field doesn't have an explicit name, use its type as name.
| UnionFieldNamesFromTypes = 0x00_00_80_00


//// Specific formats

Expand Down
38 changes: 34 additions & 4 deletions src/FSharp.SystemTextJson/Union.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace System.Text.Json.Serialization
open System
open System.Collections.Generic
open System.Text.Json
open System.Text.Json.Serialization
open System.Text.Json.Serialization.Helpers
open FSharp.Reflection

Expand Down Expand Up @@ -59,14 +60,43 @@ type JsonUnionConverter<'T>
| null -> uci.Name
| policy -> policy.ConvertName uci.Name
let fields =
uci.GetFields()
|> Array.map (fun p ->
let fields = uci.GetFields()
let usedFieldNames = Dictionary()
let fieldsAndNames =
if fsOptions.UnionEncoding.HasFlag(JsonUnionEncoding.UnionFieldNamesFromTypes) then
fields
|> Array.mapi (fun i p ->
let useTypeName =
if i = 0 && fields.Length = 1 then
p.Name = "Item"
else
p.Name = "Item" + string (i + 1)
let name =
if useTypeName then
p.PropertyType.Name
else p.Name
let nameIndex =
match usedFieldNames.TryGetValue(name) with
| true, ix -> ix + 1
| false, _ -> 1
usedFieldNames[name] <- nameIndex
p, name, nameIndex)
else
fields |> Array.map (fun p -> p, p.Name, 1)
fieldsAndNames
|> Array.map (fun (p, name, nameIndex) ->
let name =
let mutable nameCount = 1
if nameIndex = 1 && not (usedFieldNames.TryGetValue(name, &nameCount) && nameCount > 1) then
name
else
name + string nameIndex
{
Type = p.PropertyType
Name =
match options.PropertyNamingPolicy with
| null -> p.Name
| policy -> policy.ConvertName p.Name
| null -> name
| policy -> policy.ConvertName name
MustBeNonNull = not (isNullableFieldType fsOptions p.PropertyType)
MustBePresent = not (isSkippableFieldType fsOptions p.PropertyType)
IsSkip = isSkip p.PropertyType
Expand Down
39 changes: 39 additions & 0 deletions tests/FSharp.SystemTextJson.Tests/Test.Union.fs
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,23 @@ module NonStruct =
o.Converters.Add(JsonFSharpConverter(JsonUnionEncoding.InternalTag ||| JsonUnionEncoding.NamedFields, overrides = dict [typeof<Override>, JsonFSharpOptions(unionTagName = "tag")]))
Assert.Equal("""{"tag":"A","x":123,"y":"abc"}""", JsonSerializer.Serialize(Override.A(123, "abc"), o))

type NamedAfterTypes =
| NTA of int
| NTB of int * string
| NTC of x: int
| NTD of x: int * y: string
| NTE of string * string

let namedAfterTypesOptions = JsonSerializerOptions()
namedAfterTypesOptions.Converters.Add(JsonFSharpConverter(JsonUnionEncoding.InternalTag ||| JsonUnionEncoding.NamedFields ||| JsonUnionEncoding.UnionFieldNamesFromTypes))

[<Fact>]
let ``serialize UnionFieldNamesFromTypes`` () =
Assert.Equal("""{"Case":"NTA","Int32":123}""", JsonSerializer.Serialize(NTA 123, namedAfterTypesOptions))
Assert.Equal("""{"Case":"NTB","Int32":123,"String":"test"}""", JsonSerializer.Serialize(NTB(123, "test"), namedAfterTypesOptions))
Assert.Equal("""{"Case":"NTC","x":123}""", JsonSerializer.Serialize(NTC 123, namedAfterTypesOptions))
Assert.Equal("""{"Case":"NTD","x":123,"y":"test"}""", JsonSerializer.Serialize(NTD(123, "test"), namedAfterTypesOptions))
Assert.Equal("""{"Case":"NTE","String1":"123","String2":"test"}""", JsonSerializer.Serialize(NTE("123", "test"), namedAfterTypesOptions))

module Struct =

Expand Down Expand Up @@ -1249,3 +1266,25 @@ module Struct =
let o = JsonSerializerOptions()
o.Converters.Add(JsonFSharpConverter(JsonUnionEncoding.InternalTag ||| JsonUnionEncoding.NamedFields, overrides = dict [typeof<Override>, JsonFSharpOptions(unionTagName = "tag")]))
Assert.Equal("""{"tag":"A","x":123,"y":"abc"}""", JsonSerializer.Serialize(Override.A(123, "abc"), o))

[<Struct>]
type NamedAfterTypesA = NTA of int
[<Struct>]
type NamedAfterTypesB = NTB of int * string
[<Struct>]
type NamedAfterTypesC = NTC of x: int
[<Struct>]
type NamedAfterTypesD = NTD of x: int * y: string
[<Struct>]
type NamedAfterTypesE = NTE of string * string

let namedAfterTypesOptions = JsonSerializerOptions()
namedAfterTypesOptions.Converters.Add(JsonFSharpConverter(JsonUnionEncoding.InternalTag ||| JsonUnionEncoding.NamedFields ||| JsonUnionEncoding.UnionFieldNamesFromTypes))

[<Fact>]
let ``serialize UnionFieldNamesFromTypes`` () =
Assert.Equal("""{"Case":"NTA","Int32":123}""", JsonSerializer.Serialize(NTA 123, namedAfterTypesOptions))
Assert.Equal("""{"Case":"NTB","Int32":123,"String":"test"}""", JsonSerializer.Serialize(NTB(123, "test"), namedAfterTypesOptions))
Assert.Equal("""{"Case":"NTC","x":123}""", JsonSerializer.Serialize(NTC 123, namedAfterTypesOptions))
Assert.Equal("""{"Case":"NTD","x":123,"y":"test"}""", JsonSerializer.Serialize(NTD(123, "test"), namedAfterTypesOptions))
Assert.Equal("""{"Case":"NTE","String1":"123","String2":"test"}""", JsonSerializer.Serialize(NTE("123", "test"), namedAfterTypesOptions))

0 comments on commit 596e4fc

Please sign in to comment.