3

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.

5
  • 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? Commented Jun 21, 2018 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. Commented Jun 21, 2018 at 18:55
  • what's wrong with the StringConverter in the second answer you linked? You just need to implement the Write method Commented Jun 21, 2018 at 18:58
  • post your Converter code here and tell us what's wrong with it Commented Jun 21, 2018 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. Commented Jun 21, 2018 at 19:16

2 Answers 2

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;
}
Sign up to request clarification or add additional context in comments.

Comments

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();  

5 Comments

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 I see. If converters and serialization properties are too much - try pre-processing your input
I'm really trying to avoid that, but it seems I have no choice.
Might overriding JsonReader help me?
I'm not sure, it might. Another option to consider is creating some generic wrapper class NullableWhenEmpty<T>, 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<WrappedType> property {get;set;}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.