diff --git a/src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj b/src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj
index e221a36..9af7a97 100644
--- a/src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj
+++ b/src/FsCodec.SystemTextJson/FsCodec.SystemTextJson.fsproj
@@ -10,6 +10,7 @@
+
@@ -22,7 +23,7 @@
-
+
diff --git a/src/FsCodec.SystemTextJson/Options.fs b/src/FsCodec.SystemTextJson/Options.fs
index 288de0c..37e335d 100755
--- a/src/FsCodec.SystemTextJson/Options.fs
+++ b/src/FsCodec.SystemTextJson/Options.fs
@@ -60,7 +60,10 @@ type Options private () =
[] ?autoUnionToJsonObject: bool,
// Apply RejectNullStringConverter in order to have serialization throw on null strings.
// Use string option to represent strings that can potentially be null.
- [] ?rejectNullStrings: bool) =
+ [] ?rejectNullStrings: bool,
+ // Apply RejectNullConverter in order to have serialization throw on null on null or missing list or Set values.
+ // Wrap the type in option to represent values that can potentially be null or missing
+ [] ?rejectNull: bool) =
let autoTypeSafeEnumToJsonString = defaultArg autoTypeSafeEnumToJsonString false
let autoUnionToJsonObject = defaultArg autoUnionToJsonObject false
let rejectNullStrings = defaultArg rejectNullStrings false
@@ -68,6 +71,7 @@ type Options private () =
Options.CreateDefault(
converters = [|
if rejectNullStrings then RejectNullStringConverter()
+ if defaultArg rejectNull false then RejectNullConverterFactory()
if autoTypeSafeEnumToJsonString || autoUnionToJsonObject then
UnionOrTypeSafeEnumConverterFactory(typeSafeEnum = autoTypeSafeEnumToJsonString, union = autoUnionToJsonObject)
if converters <> null then yield! converters
diff --git a/src/FsCodec.SystemTextJson/RejectNullConverter.fs b/src/FsCodec.SystemTextJson/RejectNullConverter.fs
new file mode 100644
index 0000000..b81dfbd
--- /dev/null
+++ b/src/FsCodec.SystemTextJson/RejectNullConverter.fs
@@ -0,0 +1,42 @@
+namespace FsCodec.SystemTextJson
+
+open System
+open System.Linq.Expressions
+open System.Text.Json
+open System.Text.Json.Serialization
+
+type RejectNullConverter<'T>() =
+ inherit System.Text.Json.Serialization.JsonConverter<'T>()
+
+ static let defaultConverter = JsonSerializerOptions.Default.GetConverter(typeof<'T>) :?> JsonConverter<'T>
+ let msg () = sprintf "Expected value, got null. When rejectNull is true you must explicitly wrap optional %s values in an 'option'" typeof<'T>.Name
+
+ override _.HandleNull = true
+
+ override _.Read(reader, typeToConvert, options) =
+ if reader.TokenType = JsonTokenType.Null then msg () |> nullArg else
+ defaultConverter.Read(&reader, typeToConvert, options)
+ // Pretty sure the above is the correct approach (and this unsurprisingly loops, blowing the stack)
+ // JsonSerializer.Deserialize(&reader, typeToConvert, options) :?> 'T
+
+ override _.Write(writer, value, options) =
+ if value |> box |> isNull then msg () |> nullArg
+ defaultConverter.Write(writer, value, options)
+ // JsonSerializer.Serialize<'T>(writer, value, options)
+
+type RejectNullConverterFactory(predicate) =
+ inherit JsonConverterFactory()
+ new() =
+ RejectNullConverterFactory(fun (t: Type) ->
+ t.IsGenericType
+ && let gtd = t.GetGenericTypeDefinition() in gtd = typedefof> || gtd = typedefof>)
+ override _.CanConvert(t: Type) = predicate t
+
+ override _.CreateConverter(t, _options) =
+ let openConverterType = typedefof>
+ let constructor = openConverterType.MakeGenericType(t).GetConstructors() |> Array.head
+ let newExpression = Expression.New(constructor)
+ let lambda = Expression.Lambda(typeof, newExpression)
+
+ let activator = lambda.Compile() :?> ConverterActivator
+ activator.Invoke()
diff --git a/tests/FsCodec.SystemTextJson.Tests/SerdesTests.fs b/tests/FsCodec.SystemTextJson.Tests/SerdesTests.fs
index 017a153..760cebb 100644
--- a/tests/FsCodec.SystemTextJson.Tests/SerdesTests.fs
+++ b/tests/FsCodec.SystemTextJson.Tests/SerdesTests.fs
@@ -2,6 +2,7 @@ module FsCodec.SystemTextJson.Tests.SerdesTests
open System
open System.Collections.Generic
+open System.Text.Json.Serialization.Metadata
open FsCodec.SystemTextJson
open Swensen.Unquote
open Xunit
@@ -79,6 +80,32 @@ let [] ``RejectNullStringConverter rejects null strings`` () =
let value = { c = 1; d = null }
raises <@ serdes.Serialize value @>
+type WithList = { x: int; y: list }
+
+let [] ``RejectNullConverter rejects null lists and Sets`` () =
+#if false // requires WithList to be CLIMutable, which would be a big imposition
+ let tir =
+ DefaultJsonTypeInfoResolver()
+ .WithAddedModifier(fun x ->
+ // if x.Kind <> JsonTypeInfoKind.Object then
+ for p in x.Properties do
+ let pt = p.PropertyType
+ if pt.IsGenericType && (let gtd = pt.GetGenericTypeDefinition() in gtd = typedefof> || gtd = typedefof>) then
+ p.IsRequired <- true)
+ let serdes = Options.Create(TypeInfoResolver = tir) |> Serdes
+#else
+ let serdes = Options.Create(rejectNull = true) |> Serdes
+#endif
+
+ // Fails with NRE when RejectNullConverter delegates to Default list Converter
+ // seems akin to https://github.com/dotnet/runtime/issues/86483
+ let res = serdes.Deserialize """{"x":0,"y":[1]}"""
+ test <@ [1] = res.y @>
+
+ raises <@ serdes.Deserialize """{"x":0}""" @>
+ // PROBLEM: there doesn't seem to be a way to intercept explicitly passed nulls
+ // raises <@ serdes.Deserialize """{"x":0,"y":null}""" @>
+
let [] ``RejectNullStringConverter serializes strings correctly`` () =
let serdes = Serdes(Options.Create(rejectNullStrings = true))
let value = { c = 1; d = "some string" }