diff --git a/FsCodec.sln b/FsCodec.sln
index 6947a84..cd70009 100644
--- a/FsCodec.sln
+++ b/FsCodec.sln
@@ -24,6 +24,10 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsCodec", "src\FsCodec\FsCo
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsCodec.Tests", "tests\FsCodec.Tests\FsCodec.Tests.fsproj", "{0A1529E7-8DEF-4B2B-9737-3DB7BD3F1954}"
EndProject
+Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsCodec.SystemTextJson", "src\FsCodec.SystemTextJson\FsCodec.SystemTextJson.fsproj", "{1A27C90F-85EE-4AE6-A27B-183D0D50F62E}"
+EndProject
+Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FsCodec.SystemTextJson.Tests", "tests\FsCodec.SystemTextJson.Tests\FsCodec.SystemTextJson.Tests.fsproj", "{5C57C6D6-59AB-426F-9999-FDB90864545E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -82,6 +86,30 @@ Global
{0A1529E7-8DEF-4B2B-9737-3DB7BD3F1954}.Release|x64.Build.0 = Release|Any CPU
{0A1529E7-8DEF-4B2B-9737-3DB7BD3F1954}.Release|x86.ActiveCfg = Release|Any CPU
{0A1529E7-8DEF-4B2B-9737-3DB7BD3F1954}.Release|x86.Build.0 = Release|Any CPU
+ {1A27C90F-85EE-4AE6-A27B-183D0D50F62E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1A27C90F-85EE-4AE6-A27B-183D0D50F62E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1A27C90F-85EE-4AE6-A27B-183D0D50F62E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1A27C90F-85EE-4AE6-A27B-183D0D50F62E}.Debug|x64.Build.0 = Debug|Any CPU
+ {1A27C90F-85EE-4AE6-A27B-183D0D50F62E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1A27C90F-85EE-4AE6-A27B-183D0D50F62E}.Debug|x86.Build.0 = Debug|Any CPU
+ {1A27C90F-85EE-4AE6-A27B-183D0D50F62E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1A27C90F-85EE-4AE6-A27B-183D0D50F62E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1A27C90F-85EE-4AE6-A27B-183D0D50F62E}.Release|x64.ActiveCfg = Release|Any CPU
+ {1A27C90F-85EE-4AE6-A27B-183D0D50F62E}.Release|x64.Build.0 = Release|Any CPU
+ {1A27C90F-85EE-4AE6-A27B-183D0D50F62E}.Release|x86.ActiveCfg = Release|Any CPU
+ {1A27C90F-85EE-4AE6-A27B-183D0D50F62E}.Release|x86.Build.0 = Release|Any CPU
+ {5C57C6D6-59AB-426F-9999-FDB90864545E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5C57C6D6-59AB-426F-9999-FDB90864545E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5C57C6D6-59AB-426F-9999-FDB90864545E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {5C57C6D6-59AB-426F-9999-FDB90864545E}.Debug|x64.Build.0 = Debug|Any CPU
+ {5C57C6D6-59AB-426F-9999-FDB90864545E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5C57C6D6-59AB-426F-9999-FDB90864545E}.Debug|x86.Build.0 = Debug|Any CPU
+ {5C57C6D6-59AB-426F-9999-FDB90864545E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5C57C6D6-59AB-426F-9999-FDB90864545E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5C57C6D6-59AB-426F-9999-FDB90864545E}.Release|x64.ActiveCfg = Release|Any CPU
+ {5C57C6D6-59AB-426F-9999-FDB90864545E}.Release|x64.Build.0 = Release|Any CPU
+ {5C57C6D6-59AB-426F-9999-FDB90864545E}.Release|x86.ActiveCfg = Release|Any CPU
+ {5C57C6D6-59AB-426F-9999-FDB90864545E}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/build.proj b/build.proj
index 221ff26..c2ee8b8 100644
--- a/build.proj
+++ b/build.proj
@@ -14,11 +14,13 @@
+
+
diff --git a/src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj b/src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj
new file mode 100644
index 0000000..a4c09a0
--- /dev/null
+++ b/src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj
@@ -0,0 +1,33 @@
+
+
+
+ netstandard2.1
+ 5
+ false
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/FsCodec.SystemTextJson/JsonRecordConverter.fs b/src/FsCodec.SystemTextJson/JsonRecordConverter.fs
index f0c40b7..0b67d39 100644
--- a/src/FsCodec.SystemTextJson/JsonRecordConverter.fs
+++ b/src/FsCodec.SystemTextJson/JsonRecordConverter.fs
@@ -1,13 +1,11 @@
namespace FsCodec.SystemTextJson.Serialization
-open Equinox.Core
+open FSharp.Reflection
open System
open System.Collections.Generic
-open System.Linq
open System.Linq.Expressions
open System.Text.Json
open System.Text.Json.Serialization
-open FSharp.Reflection
type JsonRecordConverterActivator = delegate of JsonSerializerOptions -> JsonConverter
@@ -55,7 +53,7 @@ type JsonRecordConverter<'T> (options: JsonSerializerOptions) =
|> Array.tryHead
|> Option.map (fun attr -> (attr :?> JsonPropertyNameAttribute).Name)
|> Option.defaultWith (fun () ->
- if options.PropertyNamingPolicy |> isNull
+ if options.PropertyNamingPolicy |> isNull
then f.Name
else options.PropertyNamingPolicy.ConvertName f.Name)
@@ -92,7 +90,7 @@ type JsonRecordConverter<'T> (options: JsonSerializerOptions) =
|> Array.map KeyValuePair
|> (fun kvp -> kvp.ToDictionary((fun item -> item.Key), (fun item -> item.Value), StringComparer.OrdinalIgnoreCase))
#endif
-
+
let tryGetFieldByName name =
match fieldsByName.TryGetValue(name) with
| true, field -> Some field
@@ -107,7 +105,7 @@ type JsonRecordConverter<'T> (options: JsonSerializerOptions) =
reader.ValidateTokenType(JsonTokenType.PropertyName)
match tryGetFieldByName <| reader.GetString() with
- | Some field ->
+ | Some field ->
fields.[field.index] <-
match field.converter with
| Some converter ->
@@ -115,7 +113,7 @@ type JsonRecordConverter<'T> (options: JsonSerializerOptions) =
converter.Read(&reader, field.fieldType, options)
| None ->
JsonSerializer.Deserialize(&reader, field.fieldType, options)
- | _ ->
+ | _ ->
reader.Skip()
constructor fields :?> 'T
diff --git a/src/FsCodec.SystemTextJson/Options.fs b/src/FsCodec.SystemTextJson/Options.fs
old mode 100644
new mode 100755
index 6867c76..2c295ac
--- a/src/FsCodec.SystemTextJson/Options.fs
+++ b/src/FsCodec.SystemTextJson/Options.fs
@@ -1,14 +1,52 @@
-namespace FsCodec.SystemTextJson.Serialization
+namespace FsCodec.SystemTextJson
+open FsCodec.SystemTextJson.Serialization
+open System
+open System.Runtime.InteropServices
open System.Text.Json
+open System.Text.Json.Serialization
-[]
-module JsonSerializerOptionExtensions =
- type JsonSerializerOptions with
- static member Create() =
- let options = JsonSerializerOptions()
- options.Converters.Add(new JsonRecordConverter())
- options
+type Options private () =
-module JsonSerializer =
- let defaultOptions = JsonSerializerOptions.Create()
+ static let defaultConverters : JsonConverterFactory[] = [| JsonOptionConverter(); JsonRecordConverter() |]
+
+ /// Creates a default set of serializer options used by Json serialization. When used with no args, same as `JsonSerializerOptions()`
+ static member CreateDefault
+ ( [] converters : JsonConverterFactory[],
+ /// Use multi-line, indented formatting when serializing JSON; defaults to false.
+ [] ?indent : bool,
+ /// Render idiomatic camelCase for PascalCase items by using `PropertyNamingPolicy = CamelCase`. Defaults to false.
+ [] ?camelCase : bool,
+ /// Ignore null values in input data; defaults to false.
+ [] ?ignoreNulls : bool) =
+
+ let indent = defaultArg indent false
+ let camelCase = defaultArg camelCase false
+ let ignoreNulls = defaultArg ignoreNulls false
+ let options = JsonSerializerOptions()
+ if converters <> null then converters |> Array.iter options.Converters.Add
+ if indent then options.WriteIndented <- true
+ if camelCase then options.PropertyNamingPolicy <- JsonNamingPolicy.CamelCase; options.DictionaryKeyPolicy <- JsonNamingPolicy.CamelCase
+ if ignoreNulls then options.IgnoreNullValues <- true
+ options
+
+ /// Opinionated helper that creates serializer settings that provide good defaults for F#
+ /// - Always prepends `[JsonOptionConverter(); JsonRecordConverter()]` to any converters supplied
+ /// - no camel case conversion - assumption is you'll use records with camelCased names
+ /// Everything else is as per CreateDefault:- i.e. emit nulls instead of omitting fields, no indenting, no camelCase conversion
+ static member Create
+ ( /// List of converters to apply. Implicit [JsonOptionConverter(); JsonRecordConverter()] will be prepended and/or be used as a default
+ [] converters : JsonConverterFactory[],
+ /// Use multi-line, indented formatting when serializing JSON; defaults to false.
+ [] ?indent : bool,
+ /// Render idiomatic camelCase for PascalCase items by using `PropertyNamingPolicy = CamelCase`.
+ /// Defaults to false on basis that you'll use record and tuple field names that are camelCase (but thus not `CLSCompliant`).
+ [] ?camelCase : bool,
+ /// Ignore null values in input data; defaults to `false`.
+ [] ?ignoreNulls : bool) =
+
+ Options.CreateDefault(
+ converters = (match converters with null | [||] -> defaultConverters | xs -> Array.append defaultConverters xs),
+ ?ignoreNulls = ignoreNulls,
+ ?indent = indent,
+ ?camelCase = camelCase)
diff --git a/src/FsCodec.SystemTextJson/Serdes.fs b/src/FsCodec.SystemTextJson/Serdes.fs
new file mode 100755
index 0000000..a980509
--- /dev/null
+++ b/src/FsCodec.SystemTextJson/Serdes.fs
@@ -0,0 +1,36 @@
+namespace FsCodec.SystemTextJson
+
+open System.Runtime.InteropServices
+open System.Text.Json
+
+/// Serializes to/from strings using the Options arising from a call to Options.Create()
+type Serdes private () =
+
+ static let defaultOptions = lazy Options.Create()
+ static let indentOptions = lazy Options.Create(indent = true)
+
+ /// Serializes given value to a JSON string.
+ static member Serialize<'T>
+ ( /// Value to serialize.
+ value : 'T,
+ /// Use indentation when serializing JSON. Defaults to false.
+ [] ?indent : bool) : string =
+ let options = (if defaultArg indent false then indentOptions else defaultOptions).Value
+ JsonSerializer.Serialize(value, options)
+
+ /// Serializes given value to a JSON string with custom options
+ static member Serialize<'T>
+ ( /// Value to serialize.
+ value : 'T,
+ /// Options to use (use other overload to use Options.Create() profile)
+ options : JsonSerializerOptions) : string =
+ JsonSerializer.Serialize(value, options)
+
+ /// Deserializes value of given type from JSON string.
+ static member Deserialize<'T>
+ ( /// Json string to deserialize.
+ json : string,
+ /// Options to use (defaults to Options.Create() profile)
+ [] ?options : JsonSerializerOptions) : 'T =
+ let settings = match options with None -> defaultOptions.Value | Some x -> x
+ JsonSerializer.Deserialize<'T>(json, settings)
diff --git a/src/FsCodec.SystemTextJson/Utf8JsonReaderExtensions.fs b/src/FsCodec.SystemTextJson/Utf8JsonReaderExtensions.fs
index 9e29bb5..4c5d9c8 100644
--- a/src/FsCodec.SystemTextJson/Utf8JsonReaderExtensions.fs
+++ b/src/FsCodec.SystemTextJson/Utf8JsonReaderExtensions.fs
@@ -1,7 +1,7 @@
namespace FsCodec.SystemTextJson.Serialization
-open System.Text.Json
open System.Runtime.CompilerServices
+open System.Text.Json
[]
type Utf8JsonReaderExtension =
@@ -12,11 +12,11 @@ type Utf8JsonReaderExtension =
|> JsonException
|> raise
- []
- static member ValidatePropertyName(reader: Utf8JsonReader, expectedPropertyName: string) =
- reader.ValidateTokenType(JsonTokenType.PropertyName)
-
- if not <| reader.ValueTextEquals expectedPropertyName then
- sprintf "Expected a property named '%s', but encountered property with name '%s'." expectedPropertyName (reader.GetString())
- |> JsonException
- |> raise
+// []
+// static member ValidatePropertyName(reader: Utf8JsonReader, expectedPropertyName: string) =
+// reader.ValidateTokenType(JsonTokenType.PropertyName)
+//
+// if not <| reader.ValueTextEquals expectedPropertyName then
+// sprintf "Expected a property named '%s', but encountered property with name '%s'." expectedPropertyName (reader.GetString())
+// |> JsonException
+// |> raise
diff --git a/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj b/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj
new file mode 100644
index 0000000..dd154df
--- /dev/null
+++ b/tests/FsCodec.SystemTextJson.Tests/FsCodec.SystemTextJson.Tests.fsproj
@@ -0,0 +1,24 @@
+
+
+
+ netcoreapp3.1
+ 5
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/FsCodec.SystemTextJson.Tests/SerdesTests.fs b/tests/FsCodec.SystemTextJson.Tests/SerdesTests.fs
new file mode 100644
index 0000000..72e5718
--- /dev/null
+++ b/tests/FsCodec.SystemTextJson.Tests/SerdesTests.fs
@@ -0,0 +1,51 @@
+module FsCodec.SystemTextJson.Tests.SerdesTests
+
+open FsCodec.SystemTextJson
+open Swensen.Unquote
+open Xunit
+
+type Record = { a : int }
+
+type RecordWithOption = { a : int; b : string option }
+
+(* Characterization tests for OOTB JSON.NET
+ The aim here is to characterize the gaps that we'll shim; we only want to do that as long as it's actually earranted *)
+module StjCharacterization =
+ let ootbOptions = Options.CreateDefault()
+
+ let [] ``OOTB STJ records`` () =
+ let value = { a = 1 }
+ let ser = Serdes.Serialize(value, ootbOptions)
+ test <@ ser = """{"a":1}""" @>
+
+ let res = try let v = Serdes.Deserialize(ser, ootbOptions) in Choice1Of2 v with e -> Choice2Of2 e.Message
+ test <@ match res with
+ | Choice1Of2 v -> v = value
+ | Choice2Of2 m -> m.Contains "Deserialization of reference types without parameterless constructor is not supported. Type 'FsCodec.SystemTextJson.Tests.SerdesTests+Record'" @>
+
+ let [] ``OOTB STJ options`` () =
+ let ootbOptionsWithRecordConverter = Options.CreateDefault(converters = [|Serialization.JsonRecordConverter()|])
+ let value = { a = 1; b = Some "str" }
+ let ser = Serdes.Serialize(value, ootbOptions)
+ test <@ ser = """{"a":1,"b":{"Value":"str"}}""" @>
+ let correctSer = """{"a":1,"b":"str"}"""
+ let res = try let v = Serdes.Deserialize(correctSer, ootbOptionsWithRecordConverter) in Choice1Of2 v with e -> Choice2Of2 e.Message
+ test <@ match res with
+ | Choice1Of2 v -> v = value
+ | Choice2Of2 m -> m.Contains "The JSON value could not be converted to Microsoft.FSharp.Core.FSharpOption`1[System.String]" @>
+
+(* Serdes + default Options behavior, i.e. the stuff we do *)
+
+let [] records () =
+ let value = { a = 1 }
+ let res = Serdes.Serialize value
+ test <@ res = """{"a":1}""" @>
+ let des = Serdes.Deserialize res
+ test <@ value = des @>
+
+let [] options () =
+ let value = { a = 1; b = Some "str" }
+ let ser = Serdes.Serialize value
+ test <@ ser = """{"a":1,"b":"str"}""" @>
+ let des = Serdes.Deserialize ser
+ test <@ value = des @>