Skip to content

Commit

Permalink
Merge pull request #110 from Tarmil/unionFieldNamesFromTypes
Browse files Browse the repository at this point in the history
[#108] Add UnionFieldNamesFromTypes
  • Loading branch information
Tarmil authored Jun 8, 2022
2 parents 2fc5b39 + ab70a9a commit c376a02
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 8 deletions.
6 changes: 4 additions & 2 deletions src/FSharp.SystemTextJson/All.fs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ type JsonFSharpConverter
unionFieldsName: JsonUnionFieldsName,
[<Optional; DefaultParameterValue(Default.UnionTagNamingPolicy)>]
unionTagNamingPolicy: JsonNamingPolicy,
[<Optional; DefaultParameterValue(Default.UnionTagNamingPolicy)>]
unionFieldNamingPolicy: JsonNamingPolicy,
[<Optional; DefaultParameterValue(Default.UnionTagCaseInsensitive)>]
unionTagCaseInsensitive: bool,
[<Optional; DefaultParameterValue(Default.AllowNullFields)>]
Expand All @@ -54,7 +56,7 @@ type JsonFSharpConverter
[<Optional>]
overrides: IDictionary<Type, JsonFSharpOptions>
) =
JsonFSharpConverter(JsonFSharpOptions(unionEncoding, unionTagName, unionFieldsName, unionTagNamingPolicy, unionTagCaseInsensitive, allowNullFields, allowOverride), overrides)
JsonFSharpConverter(JsonFSharpOptions(unionEncoding, unionTagName, unionFieldsName, unionTagNamingPolicy, unionFieldNamingPolicy, unionTagCaseInsensitive, allowNullFields, allowOverride), overrides)

[<AttributeUsage(AttributeTargets.Class ||| AttributeTargets.Struct)>]
type JsonFSharpConverterAttribute
Expand All @@ -74,7 +76,7 @@ type JsonFSharpConverterAttribute

let options = JsonSerializerOptions()

let fsOptions = JsonFSharpOptions(unionEncoding, unionTagName, unionFieldsName, Default.UnionTagNamingPolicy, unionTagCaseInsensitive, allowNullFields, false)
let fsOptions = JsonFSharpOptions(unionEncoding, unionTagName, unionFieldsName, Default.UnionTagNamingPolicy, Default.UnionFieldNamingPolicy, unionTagCaseInsensitive, allowNullFields, false)

override _.CreateConverter(typeToConvert) =
JsonFSharpConverter.CreateConverter(typeToConvert, options, fsOptions, null)
Expand Down
11 changes: 10 additions & 1 deletion 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 All @@ -101,6 +104,8 @@ module internal Default =
let [<Literal>] UnionFieldsName = "Fields"

let [<Literal>] UnionTagNamingPolicy = null : JsonNamingPolicy

let [<Literal>] UnionFieldNamingPolicy = null : JsonNamingPolicy

let [<Literal>] UnionTagCaseInsensitive = false

Expand All @@ -116,6 +121,8 @@ type JsonFSharpOptions
unionFieldsName: JsonUnionFieldsName,
[<Optional; DefaultParameterValue(Default.UnionTagNamingPolicy)>]
unionTagNamingPolicy: JsonNamingPolicy,
[<Optional; DefaultParameterValue(Default.UnionTagNamingPolicy)>]
unionFieldNamingPolicy: JsonNamingPolicy,
[<Optional; DefaultParameterValue(Default.UnionTagCaseInsensitive)>]
unionTagCaseInsensitive: bool,
[<Optional; DefaultParameterValue(Default.AllowNullFields)>]
Expand All @@ -132,14 +139,16 @@ type JsonFSharpOptions

member this.UnionTagNamingPolicy = unionTagNamingPolicy

member this.UnionFieldNamingPolicy = unionFieldNamingPolicy

member this.UnionTagCaseInsensitive = unionTagCaseInsensitive

member this.AllowNullFields = allowNullFields

member this.AllowOverride = allowOverride

member this.WithUnionEncoding(unionEncoding) =
JsonFSharpOptions(unionEncoding, unionTagName, unionFieldsName, unionTagNamingPolicy, unionTagCaseInsensitive, allowNullFields, allowOverride)
JsonFSharpOptions(unionEncoding, unionTagName, unionFieldsName, unionTagNamingPolicy, unionFieldNamingPolicy, unionTagCaseInsensitive, allowNullFields, allowOverride)

type IJsonFSharpConverterAttribute =
abstract Options: JsonFSharpOptions
44 changes: 39 additions & 5 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,47 @@ 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
let policy =
match fsOptions.UnionFieldNamingPolicy with
| null -> options.PropertyNamingPolicy
| policy -> policy
match policy with
| null -> name
| policy -> policy.ConvertName name
MustBeNonNull = not (isNullableFieldType fsOptions p.PropertyType)
MustBePresent = not (isSkippableFieldType fsOptions p.PropertyType)
IsSkip = isSkip p.PropertyType
Expand Down
97 changes: 97 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,52 @@ 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))

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

let namedAfterTypesOptionsWithNamingPolicy = JsonSerializerOptions()
namedAfterTypesOptionsWithNamingPolicy.Converters.Add(
JsonFSharpConverter(JsonUnionEncoding.InternalTag ||| JsonUnionEncoding.NamedFields ||| JsonUnionEncoding.UnionFieldNamesFromTypes,
unionFieldNamingPolicy = JsonNamingPolicy.CamelCase))

[<Fact>]
let ``serialize UnionFieldNamesFromTypes with unionFieldNamingPolicy`` () =
Assert.Equal("""{"Case":"NTA","int32":123}""", JsonSerializer.Serialize(NTA 123, namedAfterTypesOptionsWithNamingPolicy))
Assert.Equal("""{"Case":"NTB","int32":123,"string":"test"}""", JsonSerializer.Serialize(NTB(123, "test"), namedAfterTypesOptionsWithNamingPolicy))
Assert.Equal("""{"Case":"NTC","x":123}""", JsonSerializer.Serialize(NTC 123, namedAfterTypesOptionsWithNamingPolicy))
Assert.Equal("""{"Case":"NTD","x":123,"y":"test"}""", JsonSerializer.Serialize(NTD(123, "test"), namedAfterTypesOptionsWithNamingPolicy))
Assert.Equal("""{"Case":"NTE","string1":"123","string2":"test"}""", JsonSerializer.Serialize(NTE("123", "test"), namedAfterTypesOptionsWithNamingPolicy))

[<Fact>]
let ``deserialize UnionFieldNamesFromTypes with unionFieldNamingPolicy`` () =
Assert.Equal(NTA 123, JsonSerializer.Deserialize("""{"Case":"NTA","int32":123}""", namedAfterTypesOptionsWithNamingPolicy))
Assert.Equal(NTB(123, "test"), JsonSerializer.Deserialize("""{"Case":"NTB","int32":123,"string":"test"}""", namedAfterTypesOptionsWithNamingPolicy))
Assert.Equal(NTC 123, JsonSerializer.Deserialize("""{"Case":"NTC","x":123}""", namedAfterTypesOptionsWithNamingPolicy))
Assert.Equal(NTD(123, "test"), JsonSerializer.Deserialize("""{"Case":"NTD","x":123,"y":"test"}""", namedAfterTypesOptionsWithNamingPolicy))
Assert.Equal(NTE("123", "test"), JsonSerializer.Deserialize("""{"Case":"NTE","string1":"123","string2":"test"}""", namedAfterTypesOptionsWithNamingPolicy))

module Struct =

Expand Down Expand Up @@ -1249,3 +1295,54 @@ 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))

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

let namedAfterTypesOptionsWithNamingPolicy = JsonSerializerOptions()
namedAfterTypesOptionsWithNamingPolicy.Converters.Add(
JsonFSharpConverter(JsonUnionEncoding.InternalTag ||| JsonUnionEncoding.NamedFields ||| JsonUnionEncoding.UnionFieldNamesFromTypes,
unionFieldNamingPolicy = JsonNamingPolicy.CamelCase))

[<Fact>]
let ``serialize UnionFieldNamesFromTypes with unionFieldNamingPolicy`` () =
Assert.Equal("""{"Case":"NTA","int32":123}""", JsonSerializer.Serialize(NTA 123, namedAfterTypesOptionsWithNamingPolicy))
Assert.Equal("""{"Case":"NTB","int32":123,"string":"test"}""", JsonSerializer.Serialize(NTB(123, "test"), namedAfterTypesOptionsWithNamingPolicy))
Assert.Equal("""{"Case":"NTC","x":123}""", JsonSerializer.Serialize(NTC 123, namedAfterTypesOptionsWithNamingPolicy))
Assert.Equal("""{"Case":"NTD","x":123,"y":"test"}""", JsonSerializer.Serialize(NTD(123, "test"), namedAfterTypesOptionsWithNamingPolicy))
Assert.Equal("""{"Case":"NTE","string1":"123","string2":"test"}""", JsonSerializer.Serialize(NTE("123", "test"), namedAfterTypesOptionsWithNamingPolicy))

[<Fact>]
let ``deserialize UnionFieldNamesFromTypes with unionFieldNamingPolicy`` () =
Assert.Equal(NTA 123, JsonSerializer.Deserialize("""{"Case":"NTA","int32":123}""", namedAfterTypesOptionsWithNamingPolicy))
Assert.Equal(NTB(123, "test"), JsonSerializer.Deserialize("""{"Case":"NTB","int32":123,"string":"test"}""", namedAfterTypesOptionsWithNamingPolicy))
Assert.Equal(NTC 123, JsonSerializer.Deserialize("""{"Case":"NTC","x":123}""", namedAfterTypesOptionsWithNamingPolicy))
Assert.Equal(NTD(123, "test"), JsonSerializer.Deserialize("""{"Case":"NTD","x":123,"y":"test"}""", namedAfterTypesOptionsWithNamingPolicy))
Assert.Equal(NTE("123", "test"), JsonSerializer.Deserialize("""{"Case":"NTE","string1":"123","string2":"test"}""", namedAfterTypesOptionsWithNamingPolicy))

0 comments on commit c376a02

Please sign in to comment.