diff --git a/src/FsCodec.SystemTextJson/Options.fs b/src/FsCodec.SystemTextJson/Options.fs index 0e6b945..acd0a4a 100755 --- a/src/FsCodec.SystemTextJson/Options.fs +++ b/src/FsCodec.SystemTextJson/Options.fs @@ -18,22 +18,27 @@ type Options private () = [] ?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) = + /// Ignore null values in input data, don't render fields with null values; defaults to `false`. + [] ?ignoreNulls : bool, + /// Drop escaping of HTML-sensitive characters. defaults to `false`. + [] ?unsafeRelaxedJsonEscaping : bool) = let indent = defaultArg indent false let camelCase = defaultArg camelCase false let ignoreNulls = defaultArg ignoreNulls false + let unsafeRelaxedJsonEscaping = defaultArg unsafeRelaxedJsonEscaping 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 + if unsafeRelaxedJsonEscaping then options.Encoder <- System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping 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 + /// Opinionated helper that creates serializer settings that represent 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
+ /// - renders values with `UnsafeRelaxedJsonEscaping` - i.e. minimal escaping as per `NewtonsoftJson`
/// 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 @@ -43,11 +48,14 @@ type Options private () = /// 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) = + /// Ignore null values in input data, don't render fields with null values; defaults to `false`. + [] ?ignoreNulls : bool, + /// Drop escaping of HTML-sensitive characters. defaults to `true`. + [] ?unsafeRelaxedJsonEscaping : bool) = Options.CreateDefault( converters = (match converters with null | [||] -> defaultConverters | xs -> Array.append defaultConverters xs), ?ignoreNulls = ignoreNulls, ?indent = indent, - ?camelCase = camelCase) + ?camelCase = camelCase, + unsafeRelaxedJsonEscaping = defaultArg unsafeRelaxedJsonEscaping true) diff --git a/tests/FsCodec.SystemTextJson.Tests/SerdesTests.fs b/tests/FsCodec.SystemTextJson.Tests/SerdesTests.fs index 8b55169..4c8f38d 100644 --- a/tests/FsCodec.SystemTextJson.Tests/SerdesTests.fs +++ b/tests/FsCodec.SystemTextJson.Tests/SerdesTests.fs @@ -34,6 +34,28 @@ module StjCharacterization = | Choice1Of2 v -> v = value | Choice2Of2 m -> m.Contains "The JSON value could not be converted to Microsoft.FSharp.Core.FSharpOption`1[System.String]" @> + // System.Text.Json's JsonSerializerOptions by default escapes HTML-sensitive characters when generating JSON strings + // while this arguably makes sense as a default + // - it's not particularly relevant for event encodings + // - and is not in alignment with the FsCodec.NewtonsoftJson default options + // see https://github.com/dotnet/runtime/issues/28567#issuecomment-53581752 for lowdown + let asRequiredForExamples : System.Text.Json.Serialization.JsonConverter [] = + [| Converters.JsonOptionConverter() + Converters.JsonRecordConverter() |] + type OverescapedOptions() as this = + inherit TheoryData() + + do // OOTB System.Text.Json over-escapes HTML-sensitive characters - `CreateDefault` honors this + this.Add(Options.CreateDefault(converters = asRequiredForExamples)) // the value we use here requires two custom Converters + // Options.Create provides a simple way to override it + this.Add(Options.Create(unsafeRelaxedJsonEscaping = false)) + let [)>] ``provides various ways to use HTML-escaped encoding``(opts : System.Text.Json.JsonSerializerOptions) = + let value = { a = 1; b = Some "\"" } + let ser = Serdes.Serialize(value, opts) + test <@ ser = """{"a":1,"b":"\u0022"}""" @> + let des = Serdes.Deserialize(ser, opts) + test <@ value = des @> + (* Serdes + default Options behavior, i.e. the stuff we do *) let [] records () = @@ -49,3 +71,10 @@ let [] options () = test <@ ser = """{"a":1,"b":"str"}""" @> let des = Serdes.Deserialize ser test <@ value = des @> + +let [] ``no over-escaping`` () = + let value = { a = 1; b = Some "\"+" } + let ser = Serdes.Serialize value + test <@ ser = """{"a":1,"b":"\"+"}""" @> + let des = Serdes.Deserialize ser + test <@ value = des @>