Skip to content

Commit fb4c228

Browse files
committed
PR 702: Refactoring for GitHub OSS
New project FSharp.Json with F# serialization converters Promoted new helpers to FSharpLu Json serialization unit tests Update nuget specs Code comments, F# runtime 4.4.0 Factorize Json code with functor Cleanup doc and comments
1 parent b5fa372 commit fb4c228

36 files changed

+1855
-643
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,5 @@ ModelManifest.xml
122122
.nuget/lib/
123123
.nuget/*.nupkg
124124
.nuget/NuGet.exe
125+
.vs/*
126+
*FSharpLu.*.nupkg

.nuget/NuGet.Config

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<configuration>
33
<solution>
4-
<add key="disableSourceControlIntegration" value="true" />
4+
<add key="disableSourceControlIntegration" value="false" />
55
</solution>
66
</configuration>

FSharpLu.Json/AssemblyInfo.fs

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
namespace FSharpLu.Json.AssemblyInfo
2+
3+
open System.Reflection
4+
open System.Runtime.CompilerServices
5+
open System.Runtime.InteropServices
6+
7+
// General Information about an assembly is controlled through the following
8+
// set of attributes. Change these attribute values to modify the information
9+
// associated with an assembly.
10+
[<assembly: AssemblyTitle("FSharpLu Json Utilities for F#")>]
11+
[<assembly: AssemblyDescription("")>]
12+
[<assembly: AssemblyConfiguration("")>]
13+
[<assembly: AssemblyCompany("Microsoft")>]
14+
[<assembly: AssemblyProduct("FSharpLu.Json")>]
15+
[<assembly: AssemblyCopyright("Copyright © 2016")>]
16+
[<assembly: AssemblyTrademark("")>]
17+
[<assembly: AssemblyCulture("")>]
18+
19+
// Setting ComVisible to false makes the types in this assembly not visible
20+
// to COM components. If you need to access a type in this assembly from
21+
// COM, set the ComVisible attribute to true on that type.
22+
[<assembly: ComVisible(false)>]
23+
24+
// The following GUID is for the ID of the typelib if this project is exposed to COM
25+
[<assembly: Guid("81eaf46a-bf20-4871-bf8e-c94fd478e9e8")>]
26+
27+
// Version information for an assembly consists of the following four values:
28+
//
29+
// Major Version
30+
// Minor Version
31+
// Build Number
32+
// Revision
33+
//
34+
// You can specify all the values or you can default the Build and Revision Numbers
35+
// by using the '*' as shown below:
36+
// [<assembly: AssemblyVersion("1.0.*")>]
37+
[<assembly: AssemblyVersion("0.9.*")>]
38+
[<assembly: AssemblyFileVersion("0.9.*")>]
39+
40+
do
41+
()

FSharpLu.Json/DiscriminatedUnions.fs

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
namespace Microsoft.FSharpLu.Json
2+
3+
/// Serializers for F# discriminated unions improving upon the stock implementation by JSon.Net
4+
/// The default formatting used by Json.Net to serialize F# discriminated unions
5+
/// and Option types is too verbose. This module implements a more succinct serialization
6+
/// for those data types.
7+
8+
open Newtonsoft.Json
9+
open Microsoft.FSharp.Reflection
10+
11+
/// Improved Json converter for discriminated unions and option types ('a option).
12+
type DiscriminatedUnionJsonConverter() =
13+
inherit Newtonsoft.Json.JsonConverter()
14+
15+
override __.CanConvert(objectType:System.Type) =
16+
// Include F# discriminated unions
17+
FSharpType.IsUnion objectType
18+
// and exclude the standard FSharp lists (which are implemented as discriminated unions)
19+
&& not (objectType.IsGenericType && objectType.GetGenericTypeDefinition() = typedefof<_ list>)
20+
21+
override __.WriteJson(writer:JsonWriter, value:obj, serializer:JsonSerializer) =
22+
let t = value.GetType()
23+
// Option type?
24+
if t.IsGenericType && t.GetGenericTypeDefinition() = typedefof<option<_>> then
25+
if isNull value then
26+
()
27+
else
28+
let _,fields = FSharpValue.GetUnionFields(value, t)
29+
serializer.Serialize(writer, fields.[0])
30+
// Discriminated union
31+
else
32+
let case, fields = FSharpValue.GetUnionFields(value, t)
33+
match fields with
34+
// Field-less union case
35+
| [||] ->
36+
writer.WriteValue(sprintf "%A" value)
37+
// Case with single field
38+
| [|onefield|] ->
39+
writer.WriteStartObject()
40+
writer.WritePropertyName(case.Name)
41+
serializer.Serialize(writer, onefield)
42+
writer.WriteEndObject()
43+
// Case with list of fields
44+
| _ ->
45+
writer.WriteStartObject()
46+
writer.WritePropertyName(case.Name)
47+
serializer.Serialize(writer, fields)
48+
writer.WriteEndObject()
49+
50+
override __.ReadJson(reader:JsonReader, objectType:System.Type, existingValue:obj, serializer:JsonSerializer) =
51+
let cases = FSharpType.GetUnionCases(objectType)
52+
// Option type?
53+
if objectType.IsGenericType && objectType.GetGenericTypeDefinition() = typedefof<option<_>> then
54+
let caseNone, caseSome = cases.[0], cases.[1]
55+
let jToken = Linq.JToken.ReadFrom(reader)
56+
if jToken.Type = Linq.JTokenType.Null then
57+
FSharpValue.MakeUnion(caseNone, [||])
58+
else
59+
let nestedType = objectType.GetGenericArguments().[0]
60+
let nestedValue = jToken.ToObject(nestedType, serializer)
61+
FSharpValue.MakeUnion(caseSome, [| nestedValue |])
62+
63+
// Discriminated union
64+
else
65+
// There are three types of union cases:
66+
// | Case1 | Case2 of 'a | Case3 of 'a1 * 'a2 ... * 'an
67+
// Those are respectively serialized to Json as
68+
// "Case1"
69+
// { "Case2" : value }
70+
// { "Case3" : [v1, v2, ... vn] }
71+
72+
// Load JObject from stream
73+
let jToken = Linq.JToken.Load(reader)
74+
75+
if isNull jToken then
76+
null
77+
78+
// Type1: field-less union case
79+
elif jToken.Type = Linq.JTokenType.String then
80+
let caseName = jToken.ToString()
81+
let matchingCase =
82+
cases
83+
|> Array.tryFind
84+
(fun case -> case.Name.Equals(caseName, System.StringComparison.InvariantCultureIgnoreCase)
85+
&& (case.GetFields() |> Array.isEmpty))
86+
match matchingCase with
87+
| Some case -> FSharpValue.MakeUnion(case,[||])
88+
| None ->
89+
failwithf "Cannot parse DU field-less value: %O. Expected names: %O" caseName (System.String.Join(", ", cases |> Seq.map(fun c->c.Name)))
90+
91+
// Type 2 or 3: Case with fields
92+
elif jToken.Type = Linq.JTokenType.Object then
93+
let jObject = jToken :?> Linq.JObject
94+
let jObjectProperties = jObject.Properties()
95+
if Seq.length jObjectProperties <> 1 then
96+
failwith "Incorrect Json format for discriminated union. A DU value with fields must be serialized to a Json object with a single Json attribute"
97+
98+
let caseProperty = jObjectProperties |> Seq.head
99+
/// Lookup the DU case by name
100+
let matchingCase =
101+
cases
102+
|> Seq.tryFind (fun case -> case.Name.Equals(caseProperty.Name, System.StringComparison.InvariantCultureIgnoreCase))
103+
104+
match matchingCase with
105+
| None ->
106+
failwithf "Case %s with fields does not exist for discriminated union %s" caseProperty.Name objectType.Name
107+
108+
// Type 2: A union case with a single field: Case2 of 'a
109+
| Some case when case.GetFields().Length = 1 ->
110+
let fieldType = case.GetFields().[0].PropertyType
111+
let field = caseProperty.Value.ToObject(fieldType, serializer)
112+
FSharpValue.MakeUnion(case, [|field|])
113+
114+
// Type 3: A union case with more than one field: Case3 of 'a1 * 'a2 ... * 'an
115+
| Some case ->
116+
// Here there could be an ambiguity:
117+
// the Json values are either the fields of the case
118+
// or if the array is Use target type to resolve ambiguity
119+
let fields =
120+
case.GetFields()
121+
|> Seq.zip caseProperty.Value
122+
|> Seq.map (fun (v,t) -> v.ToObject(t.PropertyType, serializer))
123+
|> Seq.toArray
124+
FSharpValue.MakeUnion(case, fields)
125+
else
126+
failwithf "Unexpected Json token type %O: %O" jToken.Type jToken
127+
128+
/// Serialization settings for our custom Json converter
129+
type UnionSettings =
130+
static member settings =
131+
let converter = DiscriminatedUnionJsonConverter()
132+
let s = JsonSerializerSettings(NullValueHandling = NullValueHandling.Ignore)
133+
s.Converters.Add(converter)
134+
s
135+
136+
/// Our Json serializer
137+
type Union = With<UnionSettings>

FSharpLu.Json/FSharpLu.Json.fsproj

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project ToolsVersion="15.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3+
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
4+
<PropertyGroup>
5+
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
6+
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
7+
<SchemaVersion>2.0</SchemaVersion>
8+
<ProjectGuid>81eaf46a-bf20-4871-bf8e-c94fd478e9e8</ProjectGuid>
9+
<OutputType>Library</OutputType>
10+
<RootNamespace>FSharpLu.Json</RootNamespace>
11+
<AssemblyName>FSharpLu.Json</AssemblyName>
12+
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
13+
<TargetFSharpCoreVersion>4.4.0.0</TargetFSharpCoreVersion>
14+
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
15+
<Name>FSharpLu.Json</Name>
16+
<TargetFrameworkProfile />
17+
</PropertyGroup>
18+
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
19+
<DebugSymbols>true</DebugSymbols>
20+
<DebugType>full</DebugType>
21+
<Optimize>false</Optimize>
22+
<Tailcalls>false</Tailcalls>
23+
<OutputPath>bin\Debug\</OutputPath>
24+
<DefineConstants>DEBUG;TRACE</DefineConstants>
25+
<WarningLevel>3</WarningLevel>
26+
<DocumentationFile>bin\Debug\FSharpLu.Json.XML</DocumentationFile>
27+
</PropertyGroup>
28+
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
29+
<DebugType>pdbonly</DebugType>
30+
<Optimize>true</Optimize>
31+
<Tailcalls>true</Tailcalls>
32+
<OutputPath>bin\Release\</OutputPath>
33+
<DefineConstants>TRACE</DefineConstants>
34+
<WarningLevel>3</WarningLevel>
35+
<DocumentationFile>bin\Release\FSharpLu.Json.XML</DocumentationFile>
36+
</PropertyGroup>
37+
<PropertyGroup>
38+
<MinimumVisualStudioVersion Condition="'$(MinimumVisualStudioVersion)' == ''">11</MinimumVisualStudioVersion>
39+
</PropertyGroup>
40+
<Choose>
41+
<When Condition="'$(VisualStudioVersion)' == '11.0'">
42+
<PropertyGroup Condition="Exists('$(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets')">
43+
<FSharpTargetsPath>$(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets</FSharpTargetsPath>
44+
</PropertyGroup>
45+
</When>
46+
<Otherwise>
47+
<PropertyGroup Condition="Exists('$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets')">
48+
<FSharpTargetsPath>$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets</FSharpTargetsPath>
49+
</PropertyGroup>
50+
</Otherwise>
51+
</Choose>
52+
<Import Project="$(FSharpTargetsPath)" />
53+
<ItemGroup>
54+
<Compile Include="AssemblyInfo.fs" />
55+
<Compile Include="Json.fs" />
56+
<Compile Include="DiscriminatedUnions.fs" />
57+
<Content Include="packages.config" />
58+
<None Include="FSharpLu.Json.nuspec" />
59+
</ItemGroup>
60+
<ItemGroup>
61+
<Reference Include="mscorlib" />
62+
<Reference Include="FSharp.Core, Version=$(TargetFSharpCoreVersion), Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
63+
<Private>True</Private>
64+
</Reference>
65+
<Reference Include="Newtonsoft.Json">
66+
<HintPath>..\packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
67+
<Private>True</Private>
68+
</Reference>
69+
<Reference Include="System" />
70+
<Reference Include="System.Core" />
71+
</ItemGroup>
72+
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
73+
Other similar extension points exist, see Microsoft.Common.targets.
74+
<Target Name="BeforeBuild">
75+
</Target>
76+
<Target Name="AfterBuild">
77+
</Target>
78+
-->
79+
</Project>

FSharpLu.Json/FSharpLu.Json.nuspec

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0"?>
2+
<package >
3+
<metadata>
4+
<id>$id$</id>
5+
<version>$version$</version>
6+
<title>$title$</title>
7+
<authors>[email protected]</authors>
8+
<owners>[email protected]</owners>
9+
<licenseUrl>https://github.com/Microsoft/fsharplu/blob/master/LICENSE.MD</licenseUrl>
10+
<projectUrl>https://github.com/Microsoft/fsharplu</projectUrl>
11+
<requireLicenseAcceptance>true</requireLicenseAcceptance>
12+
<description>Json serialization converters for F# option types and discriminated unions.</description>
13+
<releaseNotes>Initial release</releaseNotes>
14+
<copyright>Copyright 2016</copyright>
15+
<tags>F#, FSharp, Utilities, Json, discriminated unions</tags>
16+
</metadata>
17+
</package>

FSharpLu.Json/Json.fs

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
namespace Microsoft.FSharpLu.Json
2+
3+
open Newtonsoft.Json
4+
5+
/// Json serialization helpers for specific serializer settings
6+
type With< ^S when ^S : (static member settings : JsonSerializerSettings) > =
7+
8+
/// Serialize an object to Json with the specified converter
9+
static member inline public serialize (obj:'T) =
10+
let settings = (^S:(static member settings : JsonSerializerSettings)())
11+
JsonConvert.SerializeObject(obj, Formatting.Indented, settings)
12+
13+
/// Serialize an object to Json with the specified converter and save the result to a file
14+
static member inline public serializeToFile file (obj:'T) =
15+
let settings = (^S:(static member settings : JsonSerializerSettings)())
16+
let json = JsonConvert.SerializeObject(obj, Formatting.Indented, settings)
17+
System.IO.File.WriteAllText(file, json)
18+
19+
/// Deserialize a Json to an object of type 'T
20+
static member inline public deserialize<'T> json =
21+
let settings = (^S:(static member settings : JsonSerializerSettings)())
22+
JsonConvert.DeserializeObject<'T>(json, settings)
23+
24+
/// Deserialize a stream to an object of type 'T
25+
static member inline public deserializeStream<'T> (stream:System.IO.Stream) =
26+
let settings = (^S:(static member settings : JsonSerializerSettings)())
27+
let serializer = JsonSerializer.Create(settings)
28+
use streamReader = new System.IO.StreamReader(stream)
29+
use jsonTextReader = new JsonTextReader(streamReader)
30+
serializer.Deserialize<'T>(jsonTextReader)
31+
32+
/// Read Json from a file and desrialized it to an object of type 'T
33+
static member inline deserializeFile<'T> file =
34+
let settings = (^S:(static member settings : JsonSerializerSettings)())
35+
System.IO.File.ReadAllText file |> With< ^S>.deserialize
36+
37+
/// Try to deserialize a stream to an object of type 'T
38+
static member inline tryDeserializeStream<'T> stream =
39+
let settings = (^S:(static member settings : JsonSerializerSettings)())
40+
try
41+
let o = With< ^S>.deserializeStream<'T> stream
42+
if obj.ReferenceEquals(o, null) then
43+
Choice2Of2 <| "Deserialization returned null"
44+
else
45+
Choice1Of2 o
46+
with
47+
| :? JsonReaderException
48+
| :? JsonSerializationException as exn ->
49+
Choice2Of2 <| sprintf "Json exception thrown while deserializing stream: %O" exn
50+
| exn ->
51+
Choice2Of2 <| sprintf "Exception while deserializing stream: %O" exn
52+
53+
/// Try to deserialize json to an object of type 'T
54+
static member inline tryDeserialize<'T> json =
55+
let settings = (^S:(static member settings : JsonSerializerSettings)())
56+
try
57+
let o = With< ^S>.deserialize<'T> json
58+
if obj.ReferenceEquals(o, null) then
59+
Choice2Of2 <| "Deserialization returned null"
60+
else
61+
Choice1Of2 o
62+
with
63+
| :? JsonReaderException
64+
| :? JsonSerializationException as exn ->
65+
Choice2Of2 <| sprintf "Json exception thrown while deserializing string: %O" exn
66+
| exn ->
67+
Choice2Of2 <| sprintf "Exception while deserializing string: %O" exn
68+
69+
/// Try to read Json from a file and desrialized it to an object of type 'T
70+
static member inline tryDeserializeFile<'T> file =
71+
let settings = (^S:(static member settings : JsonSerializerSettings)())
72+
try
73+
let o = With< ^S>.deserializeFile<'T> file
74+
if obj.ReferenceEquals(o, null) then
75+
Choice2Of2 <| sprintf "Deserialization of %s returned null" file
76+
else
77+
Choice1Of2 o
78+
with
79+
| :? JsonReaderException
80+
| :? JsonSerializationException as exn ->
81+
Choice2Of2 <| sprintf "Json exception thrown while deserializing file %s: %O" file exn
82+
| exn ->
83+
Choice2Of2 <| sprintf "Exception while deserializing file %s: %O" file exn
84+
85+
/// Default serialization settings
86+
type DefaultSettings =
87+
static member settings =
88+
let s = JsonSerializerSettings(NullValueHandling = NullValueHandling.Ignore)
89+
s.Converters.Add(Converters.StringEnumConverter())
90+
s
91+
92+
type Default = With<DefaultSettings>

FSharpLu.Json/packages.config

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<packages>
3+
<package id="Newtonsoft.Json" version="8.0.3" targetFramework="net452" />
4+
</packages>

0 commit comments

Comments
 (0)