You can do this with a custom contract resolver such as the following:
type ParameterizedConstructorInitializingContractResolver() =
inherit DefaultContractResolver()
// List is a module not a static class so it's a little inconvenient to access via reflection. Use this wrapper instead.
static member EmptyList<'T>() = List.empty<'T>
override __.CreatePropertyFromConstructorParameter(matchingMemberProperty : JsonProperty, parameterInfo : ParameterInfo) =
let property = base.CreatePropertyFromConstructorParameter(matchingMemberProperty, parameterInfo)
if (not (matchingMemberProperty = null) && property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() = typedefof<_ list>) then
let genericMethod = typeof<ParameterizedConstructorInitializingContractResolver>.GetMethod("EmptyList", BindingFlags.Public ||| BindingFlags.NonPublic ||| BindingFlags.Static)
let concreteMethod = genericMethod.MakeGenericMethod(property.PropertyType.GetGenericArguments())
let defaultValue = concreteMethod.Invoke(null, null)
property.DefaultValue <- defaultValue
property.DefaultValueHandling <- new System.Nullable<DefaultValueHandling>(DefaultValueHandling.Populate)
matchingMemberProperty.DefaultValue <- defaultValue
matchingMemberProperty.DefaultValueHandling <- new System.Nullable<DefaultValueHandling>(DefaultValueHandling.Populate)
property
And then use it as follows:
let settings = JsonSerializerSettings(ContractResolver = new ParameterizedConstructorInitializingContractResolver())
let myrecord1 = JsonConvert.DeserializeObject<MyRecord>("""{ "Name": "Missing SomeList"}""", settings )
let myrecord2 = JsonConvert.DeserializeObject<MyRecord>("""{ "Name": "Populated SomeList", "SomeList" : ["a", "b", "c"]}""", settings )
let myrecord3 = JsonConvert.DeserializeObject<MyRecord>("""{ "Name": "null SomeList", "SomeList" : null}""", settings )
Notes:
The contract resolver works for any object that is deserialized via a parameterized constructor, which includes, but is not limited to, f# records. If any such object has a constructor argument with type T list
for any T
then the value will default to List.empty<T>
when missing or null.
The contract resolver reuses the same instance of the default value List.empty<T>
for all deserialized objects, which is fine here since f# lists are immutable (and List.empty<T>
seems to be a singleton anyway). The same approach would not work for providing a default value for mutable collections as a constructor argument.
You may want to cache the contract resolver for best performance.
The constructor parameter must have the same name (modulo case) as the corresponding property.
Demo fiddle here.