2

I have an endpoint that returns all null values as empty strings, even when the type is completely different. For example, this data

[{
    "field1": 3,
    "field2": "bob",
    "field3": ["alpha", "beta", "gamma"],
    "field4": { "some": "data" }
},
{
    "field1": "", // needs to be deserialized as null
    "field2": "", // same
    "field3": "", // ..
    "field4": "" // ..
}]

would need to be serialized to (an array of) a model like:

public class Root
{
    public int? Field1 { get; set; }
    public string Field2 { get; set; }
    public string[] Field3 { get; set; }
    public JObject Field4 { get; set; }
}

But Json.Net throws an exception:

Unhandled Exception: Newtonsoft.Json.JsonSerializationException: Error setting value to 'Field4' on 'Root'. ---> System.InvalidCastException: Unable to cast object of type 'Newtonsoft.Json.Linq.JValue' to type 'Newtonsoft.Json.Linq.JObject'.

I have tried using Contracts, ValueProviders, and Converters with no luck. How could I go about doing this?

None of these links has helped me:
Customize Json.NET serialization to consider empty strings as null
Convert empty strings to null with Json.Net
Json Convert empty string instead of null

EDIT1: Fixed typo.

EDIT2: This is the code for the converter I tried using:

public class VdfNullConverter : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        // ...
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.TokenType == JsonToken.String && (reader.Value as string == ""))
            return null;

        // I don't know what to do here
    }

    public override bool CanConvert(Type objectType)
    {
        return true;
    }
}

My issue is that I don't know how to handle the case where the data is in fact not an empty string. In that case, I need the other converters to be called, but I have no way to "cancel" halfway through ReadJson.

Hele
  • 1,558
  • 4
  • 23
  • 39
  • I'm surprised the other fields aren't tripping up as well. I would think `public int Field1` should be `public int? Field1`, no? What happens if you make `Field4` nullable? – Jonathan Jun 21 '18 at 18:50
  • The `int -> int?` issue was a typo. Sorry about that. I've fixed it now. I can't make `Field4` nullable since it already is. JObject is a subtype of object. The other fields trip up too, one by one, when I remove `Field4`. – Hele Jun 21 '18 at 18:55
  • what's wrong with the StringConverter in the second answer you linked? You just need to implement the Write method – Sten Petrov Jun 21 '18 at 18:58
  • post your Converter code here and tell us what's wrong with it – Sten Petrov Jun 21 '18 at 19:12
  • @StenPetrov Just added it. Lmk if you need me to expand on it. The main issue with the StringConverter in link 2 is that it only works for string objects. I need it to work for all strings/numbers/arrays/objects. – Hele Jun 21 '18 at 19:16

2 Answers2

1

I finally found a simple, but hacky solution. I used a regular JsonConverter and backtracked using code like this:

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
    if (reader.TokenType == JsonToken.String && (reader.Value as string == ""))
        return null;

    skip = true;
    return serializer.Deserialize(reader, objectType);
}

private bool skip = false;
public override bool CanConvert(Type objectType) // If this is ever cached, this hack won't work.
{
    if (skip)
    {
        skip = false;
        return false;
    }

    return true;
}
Hele
  • 1,558
  • 4
  • 23
  • 39
0

If your model really consists of just 4 fields you can consider using serialization properties

public class Root{

  [JsonIgnore]
  public int? Field1 {get;set;}

  [JsonProperty("field1")]
  protected string Field1Data{
     {
        get { return Field1?.ToString(); }
        set { 
           if (string.IsNullOrEmpty(value))
              Field1 = null;
           else {
              int parsed;
              if (Int32.TryParse(value, out parsed)){
                 Field1 = parsed;
              } else {
                 throw new ArgumentException($"{value} could not be parsed", nameof(Field1));
              }
           }
        } 
     }
  } 
}

If your model has a larger variety of types and fields then you'll have to use a custom json converter.

Either way, the JObject in your sample looks suspicious. It will likely need to be of some other type, such as Object.

EDIT:

Since you have a very large variety of types you may want to consider pre-processing your json before deserializing it, since using converters would require more specificity.

You could parse your input as a JObject, select all the empty string values and delete the properties:

  var jobj = JObject.Parse(File.ReadAllText("sample.json"));
  var tokens = jobj.SelectTokens("$..[?(@=~/^$/)]").ToList();
  tokens.ForEach(t=>t.Parent.Remove()); // you could filter some of the removals if you need to 
  string nowUseThisAsYourImpup = jobj.ToString();  
Sten Petrov
  • 10,943
  • 1
  • 41
  • 61
  • I have models of different types, some very large and nested. So using serialization properties is not feasible. I have tried using a custom converter, but that did not help my case either. I can post the code if you like. I'm fairly sure the JObject is correct usage. It's a way to avoid using `dynamic` on a regular `object`. – Hele Jun 21 '18 at 19:13
  • @Hele I see. If converters and serialization properties are too much - try pre-processing your input – Sten Petrov Jun 21 '18 at 20:09
  • I'm really trying to avoid that, but it seems I have no choice. – Hele Jun 21 '18 at 20:21
  • Might overriding `JsonReader` help me? – Hele Jun 21 '18 at 20:30
  • I'm not sure, it *might*. Another option to consider is creating some generic wrapper class `NullableWhenEmpty`, make a converter that converts all properties of such type. It would reduce the number of converters you need to register. Then instead of a concrete type you'd use `NullableWhenEmpty property {get;set;}` – Sten Petrov Jun 21 '18 at 20:56