How to disallow empty string when deserializing record types with newtonsoft in F#?

Viewed 239

Is there any way to force parsing of only non-empty string fields of a record type in F# using Newtonsoft.Json?

#r """Newtonsoft.Json.dll"""

open Newtonsoft.Json

type Customer = {
    Name:           string
    Email:          string
    ContactPhoneNo: string
}

// one or more fields can be empty
let customer = {
    Name =           ""
    Email =          "ca@gmail.com"
    ContactPhoneNo = "+123456789"
}

let serializedCustomer =
    JsonConvert.SerializeObject(customer)

// this parses correctly with the Name field set as ""
// But as the name field is empty, it should not parse it
let deserializedCustomer =
    JsonConvert.DeserializeObject<Customer>(serializedCustomer)
2 Answers

You might want to consider using Newtonsoft's schema support for this, which is in a separate package called Newtonsoft.Json.Schema. You can specify many different kinds of constraints using annotations. For example, to disallow blank names, you can use MinLength:

open System.ComponentModel.DataAnnotations

type Customer = {
    [<MinLength(1)>]
    Name:           string
    Email:          string
    ContactPhoneNo: string
}

Once you've annotated your type, you can generate a schema:

let generator = JSchemaGenerator()
let schema = generator.Generate(typeof<Customer>)

Then use it to validate the serialized JSON:

let jsonCustomer = JObject.Parse(serializedCustomer)
let isValid = jsonCustomer.IsValid(schema)

If you want to skip the overhead of first loading JSON into a JObject in order to validate it, you can use a JSchemaValidatingReader instead:

use strReader = new System.IO.StringReader(serializedCustomer)
use txtReader = new JsonTextReader(strReader)
use vldReader = new JSchemaValidatingReader(txtReader, Schema = schema)
let messages = ResizeArray()
vldReader.ValidationEventHandler.Add(fun args -> messages.Add(args.Message))
let serializer = JsonSerializer()
let deserializedCustomer = serializer.Deserialize<Customer>(vldReader)
printfn "%A" deserializedCustomer
let isValid = (messages.Count = 0)
printfn "%A" isValid

See this documentation for details.

You could implement a custom JsonConverter that converts values of type string, but throws an exception when the string is empty:

let nonEmptyStringConverter = 
  { new JsonConverter() with
      override x.CanConvert(objectType) = objectType = typeof<string>
      override x.WriteJson(writer, value, serializer) =
        JValue(value :?> string).WriteTo(writer)
      override x.ReadJson(reader, objectType, existingValue, serializer) =
        let jt = JToken.Load(reader)
        if jt.Type = JTokenType.String then 
          let str = jt.Value<string>() 
          if String.IsNullOrEmpty str then failwith "Empty string"
          box str
        else failwith "Expected a string" }

If you pass this to DeserializeObject, then it will throw an exception in your example:

let serializedCustomer =
    JsonConvert.SerializeObject(customer)

let deserializedCustomer =
    JsonConvert.DeserializeObject<Customer>(serializedCustomer, nonEmptyStringConverter)

One caveat is that this will apply to all string values in the type and I expect you may want to allow empty values for some. A better approach would be to define a custom type and define a convertor only for this type, e.g. using:

type NonEmptyString = NE of string

type Customer = 
  { Name : NonEmptyString
    Email: NonEmptyString
    ContactPhoneNo: string }
Related