4

I'm struggling with custom json serialization in .Net core, I'm trying to make all properties required by default except if property has specific type. Here is an example of what I'm trying to achieve:

Let's assument that I have following type: F#:

type FooType = {
   id: int
   name: string 
   optional: int option 
}

you can think about below code as similar to following in C#:

class FooType =
{
   int Id {get;set;};
   string Name {get;set;};
   Nullable<int> Optional {get;set;};
}

What I'm trying to do is to return error if Id or Name property is missing in json object but deserialize without error if Optional is missing (so basically to set Property as required or not based on it's type). I'm able to mark all properties as required by using RequireObjectPropertiesContractResolver from this sample: https://stackoverflow.com/a/29660550 but unfortunately I wasn't able to build something more dynamic.

I have also default converter for Optional types I would like to add to serialization. It's not part of this one specific question but if you have an idea how to mark property to be required or not and to use custom Converter in one place than it would be even greater.

dbc
  • 104,963
  • 20
  • 228
  • 340
white_bear
  • 73
  • 5
  • 1
    `option` is not natively recognized by JSON.NET, but `Nullable` is. Just use `Nullable`. – Fyodor Soikin Jan 18 '18 at 18:25
  • What is the specific criteria which determines whether a property should be required or not? – Brian Rogers Jan 18 '18 at 18:25
  • Are you using the `RequireObjectPropertiesContractResolver` from that answer? – dbc Jan 18 '18 at 19:25
  • @FyodorSoikin `Nullable`/`option` is not an issue here (I have custom converter to serialize/ deserialize). An issue is to mark some properties as required and some not. – white_bear Jan 18 '18 at 19:34
  • @BrianRogers Required by default for all types, not required only for `option` @dbc yes, but it sets all properties as required and I want to mark `option` as not required. – white_bear Jan 18 '18 at 19:45
  • @white_bear - OK, I think I can do that, but in return can you [edit] your question to provide a sample of `FooType` that compiles? I'm trying to port `RequireObjectPropertiesContractResolver` to f# but I'm having some trouble testing it without a [mcve] showing a working version of a sample type with an optional member. – dbc Jan 18 '18 at 19:50
  • @dbc thanks a lot, type FooType = { id: int name: string optional: int option } – white_bear Jan 18 '18 at 19:52
  • OK, but doesn't it need a constructor? I tried to instantiate it given that type definition and the compiler responded, *no constructors are available for the type `FooType`*. See https://dotnetfiddle.net/HP3inr – dbc Jan 18 '18 at 19:54
  • 1
    in your fiddle example you can change to `let foo = {FooType.id = 101; name = "John"; optional = None}` and it will work or `let foo = {FooType.id = 101; name = "John"; optional = Some 5}` https://dotnetfiddle.net/sSXHLr – white_bear Jan 18 '18 at 20:01
  • @white_bear - got it! – dbc Jan 18 '18 at 20:02
  • @white_bear - for the second part of your question, I'd suggest asking a second question. The preferred format here is [one question per post](https://meta.stackexchange.com/q/222735/344280). – dbc Jan 18 '18 at 21:09

1 Answers1

4

You can combine the contract resolver from Json.NET require all properties on deserialization with the logic from the answer to Reflection to find out if property is of option type by p.s.w.g to mark all members except those that are optional as required:

type RequireObjectPropertiesContractResolver() =
    inherit DefaultContractResolver()

    override __.CreateObjectContract(objectType: Type) = 
        let contract = base.CreateObjectContract objectType
        contract.ItemRequired <- System.Nullable<Required>(Required.Always)
        contract

    override __.CreateProperty(memberInfo: MemberInfo, memberSerialization: MemberSerialization) =
        let property = base.CreateProperty(memberInfo, memberSerialization)
        // https://stackoverflow.com/questions/20696262/reflection-to-find-out-if-property-is-of-option-type
        let isOption = property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() = typedefof<Option<_>>
        if isOption then (
            property.Required <- Required.Default        
            property.NullValueHandling <- new System.Nullable<NullValueHandling>(NullValueHandling.Ignore)
        )
        property

Then, deserialize as follows:

let settings = new JsonSerializerSettings(ContractResolver = RequireObjectPropertiesContractResolver())
let obj = JsonConvert.DeserializeObject<FooType>(inputJson, settings)

Notes:

  • I also added NullValueHandling.Ignore so that optional members with no value would not get serialized.

  • You may want to cache the contract resolver for best performance.

  • Option<'T> is not the same as Nullable<'T>. I checked for typedefof<Option<_>> but you could add a check for typedefof<System.Nullable<_>> as well if you want:

    let isOption = property.PropertyType.IsGenericType && (property.PropertyType.GetGenericTypeDefinition() = typedefof<Option<_>> || property.PropertyType.GetGenericTypeDefinition() = typedefof<System.Nullable<_>>)
    

Sample fiddle, which demonstrates that the string {"id":101,"name":"John"} can be deserialized, but the string {"id":101} cannot.

knocte
  • 16,941
  • 11
  • 79
  • 125
dbc
  • 104,963
  • 20
  • 228
  • 340