Skip to content

Commit

Permalink
Merge pull request #3 from vsapronov/single-case-union
Browse files Browse the repository at this point in the history
Single case union
  • Loading branch information
vsapronov authored Nov 25, 2017
2 parents 3553ab4 + d0656be commit ac278a2
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 52 deletions.
12 changes: 11 additions & 1 deletion FSharp.Json.sln
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,17 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{83F16175
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "content", "content", "{8E6D5255-776D-4B61-85F9-73C37AA1FB9A}"
ProjectSection(SolutionItems) = preProject
docsrc\content\index.fsx = docsrc\content\index.fsx
docsrc\content\api_overview.md = docsrc\content\api_overview.md
docsrc\content\basic_example.fsx = docsrc\content\basic_example.fsx
docsrc\content\enums.fsx = docsrc\content\enums.fsx
docsrc\content\fields_naming.fsx = docsrc\content\fields_naming.fsx
docsrc\content\index.md = docsrc\content\index.md
docsrc\content\license.md = docsrc\content\license.md
docsrc\content\null_safety.fsx = docsrc\content\null_safety.fsx
docsrc\content\release-notes.md = docsrc\content\release-notes.md
docsrc\content\supported_types.md = docsrc\content\supported_types.md
docsrc\content\transform.fsx = docsrc\content\transform.fsx
docsrc\content\unions.fsx = docsrc\content\unions.fsx
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{ED8079DD-2B06-4030-9F0F-DC548F98E1C4}"
Expand Down
3 changes: 3 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
#### 0.2
* Single case union as wrapped type

#### 0.1
* Initial release
35 changes: 32 additions & 3 deletions docsrc/content/unions.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,42 @@ let json = Json.serialize data
let deserialized = Json.deserialize<TheUnion> json
// deserialized is OneFieldCase("The string")

(**
Single case union
-----------------
Single case union is a special scenario.
Read [here](https://fsharpforfunandprofit.com/posts/designing-with-types-single-case-dus/) about single case union usage.
In such case serializing union as JSON object is overkill.
It's more convenient to represent single case union the same way as a wrapped type.
Here's example of single case union serialization:
*)

#r "FSharp.Json.dll"
open FSharp.Json

// Single case union type
type TheUnion = SingleCase of string

type TheRecord = {
// value will be just a string - wrapped into union type
value: TheUnion
}

let data = { TheRecord.value = SingleCase "The string" }

let json = Json.serialize data
// json is """{"value":"The string"}"""

let deserialized = Json.deserialize<TheRecord> json
// deserialized is { TheRecord.value = SingleCase "The string" }

(**
Union modes
-----------
All examples above are describing default serialization of union into JSON.
This mode is known as "case key as a field name" mode.
There's another [union mode](reference/fsharp-json-unionmode.html) that represents union as JSON object with two fields.
There's [union mode](reference/fsharp-json-unionmode.html) that represents union as JSON object with two fields.
One field is for case key and another one is for case value. This mode is called "case key as a field value"
If this mode is used then names of these two field should be provided through [JsonUnion attribute](reference/fsharp-json-jsonunion.html).
Expand Down
8 changes: 4 additions & 4 deletions src/FSharp.Json/AssemblyInfo.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ open System.Reflection
[<assembly: AssemblyTitleAttribute("FSharp.Json")>]
[<assembly: AssemblyProductAttribute("FSharp.Json")>]
[<assembly: AssemblyDescriptionAttribute("F# JSON Reflection based serialization library")>]
[<assembly: AssemblyVersionAttribute("0.1")>]
[<assembly: AssemblyFileVersionAttribute("0.1")>]
[<assembly: AssemblyVersionAttribute("0.2")>]
[<assembly: AssemblyFileVersionAttribute("0.2")>]
[<assembly: AssemblyConfigurationAttribute("Release")>]
do ()

module internal AssemblyVersionInformation =
let [<Literal>] AssemblyTitle = "FSharp.Json"
let [<Literal>] AssemblyProduct = "FSharp.Json"
let [<Literal>] AssemblyDescription = "F# JSON Reflection based serialization library"
let [<Literal>] AssemblyVersion = "0.1"
let [<Literal>] AssemblyFileVersion = "0.1"
let [<Literal>] AssemblyVersion = "0.2"
let [<Literal>] AssemblyFileVersion = "0.2"
let [<Literal>] AssemblyConfiguration = "Release"
109 changes: 65 additions & 44 deletions src/FSharp.Json/Core.fs
Original file line number Diff line number Diff line change
Expand Up @@ -162,23 +162,27 @@ module internal Core =
let serializeUnion (t: Type) (theunion: obj): JsonValue =
let caseInfo, values = FSharpValue.GetUnionFields(theunion, t)
let jsonField = getJsonFieldUnionCase caseInfo
let jsonUnionCase = getJsonUnionCase caseInfo
let jsonUnion = getJsonUnion caseInfo.DeclaringType
let jvalue =
match values.Length with
| 1 ->
let caseValue = values.[0]
serializeNonOption (caseValue.GetType()) jsonField caseValue
| _ ->
serializeEnumerable values
let theCase = getJsonUnionCaseName config jsonUnion jsonUnionCase caseInfo
match jsonUnion.Mode with
| UnionMode.CaseKeyAsFieldName -> JsonValue.Record [| (theCase, jvalue) |]
| UnionMode.CaseKeyAsFieldValue ->
let jkey = (jsonUnion.CaseKeyField, JsonValue.String theCase)
let jvalue = (jsonUnion.CaseValueField, jvalue)
JsonValue.Record [| jkey; jvalue |]
| mode -> failSerialization <| sprintf "Failed to serialize union, unsupported union mode: %A" mode
let unionCases = getUnionCases caseInfo.DeclaringType
match unionCases.Length with
| 1 -> jvalue
| _ ->
let jsonUnionCase = getJsonUnionCase caseInfo
let jsonUnion = getJsonUnion caseInfo.DeclaringType
let theCase = getJsonUnionCaseName config jsonUnion jsonUnionCase caseInfo
match jsonUnion.Mode with
| UnionMode.CaseKeyAsFieldName -> JsonValue.Record [| (theCase, jvalue) |]
| UnionMode.CaseKeyAsFieldValue ->
let jkey = (jsonUnion.CaseKeyField, JsonValue.String theCase)
let jvalue = (jsonUnion.CaseValueField, jvalue)
JsonValue.Record [| jkey; jvalue |]
| mode -> failSerialization <| sprintf "Failed to serialize union, unsupported union mode: %A" mode

match t with
| t when isRecord t -> serializeRecord t value
Expand Down Expand Up @@ -362,50 +366,67 @@ module internal Core =

let deserializeUnion (path: JsonPath) (t: Type) (jvalue: JsonValue): obj =
let jsonUnion = getJsonUnion t
match jvalue with
| JsonValue.Record fields ->
let fieldName, fieldValue =
match jsonUnion.Mode with
| UnionMode.CaseKeyAsFieldName ->
if fields.Length <> 1 then
failDeserialization path <| sprintf "Failed to parse union from record with %i fields, should be 1 field." fields.Length
fields.[0]
| UnionMode.CaseKeyAsFieldValue ->
if fields.Length <> 2 then
failDeserialization path <| sprintf "Failed to parse union from record with %i fields, should be 2 fields." fields.Length
let caseKeyField = fields |> Seq.tryFind (fun f -> fst f = jsonUnion.CaseKeyField)
let caseKeyField =
match caseKeyField with
| Some fieldName -> fieldName
| None -> failDeserialization path <| sprintf "Failed to parse union, unable to find union case field: %s." jsonUnion.CaseKeyField
let caseValueField = fields |> Seq.tryFind (fun f -> fst f = jsonUnion.CaseValueField)
let caseValueField =
match caseValueField with
| Some fieldValue -> fieldValue
| None -> failDeserialization path <| sprintf "Failed to parse union, unable to find union case field value: %s." jsonUnion.CaseValueField
let caseNamePath = caseKeyField |> fst |> JsonPathItem.Field |> path.createNew
let caseName = JsonValueHelpers.getString caseNamePath (snd caseKeyField)
(caseName, snd caseValueField)
| mode -> failDeserialization path <| sprintf "Failed to parse union, unsupported union mode: %A" mode
let casePath = JsonPathItem.Field fieldName |> path.createNew
let caseInfo = t |> getUnionCases |> Array.tryFind (fun c -> getJsonUnionCaseName config jsonUnion (getJsonUnionCase c) c = fieldName)
let caseInfo =
match caseInfo with
| Some caseInfo -> caseInfo
| None -> failDeserialization path <| sprintf "Failed to parse union, unable to find union case: %s." fieldName
let unionCases = t |> getUnionCases
match unionCases.Length with
| 1 ->
let caseInfo = unionCases.[0]
let fieldAttr = getJsonFieldUnionCase caseInfo
let props: PropertyInfo array = caseInfo.GetFields()
let values =
match props.Length with
| 1 ->
let propType = props.[0].PropertyType
let propValue = deserializeUnwrapOption casePath propType fieldAttr (Some fieldValue)
let propValue = deserializeUnwrapOption path propType fieldAttr (Some jvalue)
[| propValue |]
| _ ->
let propsTypes = props |> Array.map (fun p -> p.PropertyType)
deserializeTupleElements casePath propsTypes fieldValue
deserializeTupleElements path propsTypes jvalue
FSharpValue.MakeUnion (caseInfo, values)
| _ -> failDeserialization path "Failed to parse union from JSON that is not object."
| _ ->
match jvalue with
| JsonValue.Record fields ->
let fieldName, fieldValue =
match jsonUnion.Mode with
| UnionMode.CaseKeyAsFieldName ->
if fields.Length <> 1 then
failDeserialization path <| sprintf "Failed to parse union from record with %i fields, should be 1 field." fields.Length
fields.[0]
| UnionMode.CaseKeyAsFieldValue ->
if fields.Length <> 2 then
failDeserialization path <| sprintf "Failed to parse union from record with %i fields, should be 2 fields." fields.Length
let caseKeyField = fields |> Seq.tryFind (fun f -> fst f = jsonUnion.CaseKeyField)
let caseKeyField =
match caseKeyField with
| Some fieldName -> fieldName
| None -> failDeserialization path <| sprintf "Failed to parse union, unable to find union case field: %s." jsonUnion.CaseKeyField
let caseValueField = fields |> Seq.tryFind (fun f -> fst f = jsonUnion.CaseValueField)
let caseValueField =
match caseValueField with
| Some fieldValue -> fieldValue
| None -> failDeserialization path <| sprintf "Failed to parse union, unable to find union case field value: %s." jsonUnion.CaseValueField
let caseNamePath = caseKeyField |> fst |> JsonPathItem.Field |> path.createNew
let caseName = JsonValueHelpers.getString caseNamePath (snd caseKeyField)
(caseName, snd caseValueField)
| mode -> failDeserialization path <| sprintf "Failed to parse union, unsupported union mode: %A" mode
let casePath = JsonPathItem.Field fieldName |> path.createNew
let caseInfo = unionCases |> Array.tryFind (fun c -> getJsonUnionCaseName config jsonUnion (getJsonUnionCase c) c = fieldName)
let caseInfo =
match caseInfo with
| Some caseInfo -> caseInfo
| None -> failDeserialization path <| sprintf "Failed to parse union, unable to find union case: %s." fieldName
let fieldAttr = getJsonFieldUnionCase caseInfo
let props: PropertyInfo array = caseInfo.GetFields()
let values =
match props.Length with
| 1 ->
let propType = props.[0].PropertyType
let propValue = deserializeUnwrapOption casePath propType fieldAttr (Some fieldValue)
[| propValue |]
| _ ->
let propsTypes = props |> Array.map (fun p -> p.PropertyType)
deserializeTupleElements casePath propsTypes fieldValue
FSharpValue.MakeUnion (caseInfo, values)
| _ -> failDeserialization path "Failed to parse union from JSON that is not object."

match t with
| t when isRecord t -> deserializeRecord path t jvalue
Expand Down
20 changes: 20 additions & 0 deletions tests/FSharp.Json.Tests/Union.fs
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,23 @@ module Union =
let config = JsonConfig.create(jsonFieldNaming = Json.snakeCase)
let actual = Json.deserializeEx<TheUnion> config json
Assert.AreEqual(expected, actual)

type SingleCaseUnion = SingleCase of string

type SingleCaseRecord = {
value: SingleCaseUnion
}

[<Test>]
let ``Union single case serialization`` () =
let value = { SingleCaseRecord.value = SingleCase "The string" }
let actual = Json.serializeU value
let expected = """{"value":"The string"}"""
Assert.AreEqual(expected, actual)

[<Test>]
let ``Union single case deserialization`` () =
let expected = { SingleCaseRecord.value = SingleCase "The string" }
let json = Json.serialize(expected)
let actual = Json.deserialize<SingleCaseRecord> json
Assert.AreEqual(expected, actual)

0 comments on commit ac278a2

Please sign in to comment.