1

I serialize a list of Interfaces into a JSON file that I deserialize later. In one application, I am deserializing all types of JSON objects, but in another application I am wanting to deserialize only one specific implementation of the Interface (Machine1). I want to keep the way I am serializing the data as-is, but change the deserialization code to SKIP any of the incorrect objects (JSON objects of a different type than I am interested)- my plan was to do this by checking if a Member variable is missing (if powerWatts is missing, then the data must be representing Machine2, therefore we should skip deserializing this particular object (Machine2) and instead deserialize the others in the array). How would I implement this using JSON.Net? MissingMemberHandling.Error throws an exception, so I don't think what would work for deserializing the rest of the list after seeing a missing property. MissingMemberHandling.Ignore leaves me with a property equal to 0, which is not correct.

Interface and Classes:

public interface IMachineInfo
{
    DateTime windowsTime { get; set; }
    DateTime irigTime { get; set; }
    string messageTypeFlag { get; }
    byte? sequenceNum { get; }
}  

public class Machine1 : IMachineInfo
{
    // Interface properties omitted for brevity
    public double powerWatts { get; set; }
    public double radiation { get; set; }
    public double current { get; set; }
}

public class Machine2 : IMachineInfo
{
    public double dbm { get; set; }
    public double magneticField { get; set; }
    public double frequency { get; set; }
}

Main:

// Serialization: get the interface and specific data into a collection that will be written to a file

IMachineInfo decoded = data[key].MachineDataDecoded;
dataConverted[key] = decoded;

//... more code 

//write JSON data to JSONData.txt

string jsondata = JsonConvert.SerializeObject(dataConverted.Values.ToArray(), Formatting.Indented, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });

File.AppendAllText(Environment.CurrentDirectory + subDirectoryName + jsonFileName, jsondata);

App2 (deserialize the data)

// try to read the json, but I only want Machine1 data, skip anything in the json that is related to machine2

JsonSerializerSettings settings = new JsonSerializerSettings
{
    NullValueHandling = NullValueHandling.Ignore,
    MissingMemberHandling = MissingMemberHandling.Ignore
};
List<Machine1> ampData = JsonConvert.DeserializeObject<List<Machine1>>(@"C/Path/jsonfile", settings);

Real JSON Data:

[
  {
    "windowsTime": "2019-01-14T18:47:55.8390256-06:00",
    "irigTime": "0001-01-01T00:00:00",
    "messageTypeFlag": "Q",
    "sequenceNum": 0,
  },
  {
    "dbm": "66",
    "magneticField ": "8967",
    "frequency": "34500",
    "windowsTime": "2019-01-14T18:47:55.8390256-06:00",
    "irigTime": "0001-01-01T00:00:00",
    "messageTypeFlag": "Q",
    "sequenceNum": 0,
  },
  {
    "powerWatts": "4000",
    "radiation": "67",
    "current": "2478",
    "windowsTime": "2019-01-14T18:47:55.8390256-06:00",
    "irigTime": "0001-01-01T00:00:00",
    "messageTypeFlag": "Q",
    "sequenceNum": 0,
  },
  {
    "powerWatts": "4000",
    "radiation": "67",
    "current": "2478",
    "windowsTime": "2019-01-14T18:47:55.8390258-06:00",
    "irigTime": "0001-01-01T00:00:00",
    "messageTypeFlag": "Q",
    "sequenceNum": 0,
   }
]

in my data above, only the 3rd and 4th element is of type Machine1, therefore I only want those object to be added to the list from json deserialization. The problem is, that when I deserialize this now, powerWatts = 0 for all 4 elements (not the behavior I want), even though it is only a valid property of the 3rd and 4th element. This is a problem since i can't just check if powerWatts == 0 and remove it from my list since 0 could be a valid value in a real world situation. I need to only deserialize Machine1 JSON objects

dbc
  • 104,963
  • 20
  • 228
  • 340
Goku
  • 1,565
  • 1
  • 29
  • 62

1 Answers1

2

You could adopt and extend the general approach from this answer to Deserializing polymorphic json classes without type information using json.net or this answer to How to call JsonConvert.DeserializeObject and disable a JsonConverter applied to a base type via [JsonConverter]? by creating a custom JsonConverter for List<IMachineInfo> that does the following:

  1. Loads each array entry into a temporary JToken.
  2. Tries to infer the item type from the parameters present.
  3. Skips the array entry if the type cannot be inferred.

To do this, first introduce a generic base class converter as follows:

public abstract class JsonListItemTypeInferringConverterBase<TItem> : JsonConverter
{
    public override bool CanWrite { get { return false; } }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }

    protected abstract bool TryInferItemType(Type objectType, JToken json, out Type type);

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        // Get contract information
        var contract = serializer.ContractResolver.ResolveContract(objectType) as JsonArrayContract;
        if (contract == null || contract.IsMultidimensionalArray || objectType.IsArray)
            throw new JsonSerializationException(string.Format("Invalid array contract for {0}", objectType));

        if (reader.MoveToContent().TokenType == JsonToken.Null)
            return null;

        if (reader.TokenType != JsonToken.StartArray)
            throw new JsonSerializationException(string.Format("Expected {0}, encountered {1} at path {2}", JsonToken.StartArray, reader.TokenType, reader.Path));

        var collection = existingValue as IList<TItem> ?? (IList<TItem>)contract.DefaultCreator();

        // Process the collection items
        while (reader.Read())
        {
            switch (reader.TokenType)
            {
                case JsonToken.EndArray:
                    return collection;

                case JsonToken.Comment:
                    break;

                default:
                    {
                        var token = JToken.Load(reader);
                        Type itemType;
                        if (!TryInferItemType(typeof(TItem), token, out itemType))
                            break;
                        collection.Add((TItem)serializer.Deserialize(token.CreateReader(), itemType));
                    }
                    break;
            }
        }
        // Should not come here.
        throw new JsonSerializationException("Unclosed array at path: " + reader.Path);
    }

    public override bool CanConvert(Type objectType)
    {
        return objectType.IsAssignableFrom(typeof(List<TItem>));
    }
}

public static partial class JsonExtensions
{
    public static JsonReader MoveToContent(this JsonReader reader)
    {
        while ((reader.TokenType == JsonToken.Comment || reader.TokenType == JsonToken.None) && reader.Read())
            ;
        return reader;
    }
}

Then, make a concrete version that only deserializes list entries of type Machine1 as follows:

public class Machine1ListConverter<TMachineInfo> : JsonListItemTypeInferringConverterBase<TMachineInfo> where TMachineInfo : IMachineInfo
{
    protected override bool TryInferItemType(Type objectType, JToken json, out Type type)
    {
        var obj = json as JObject;
        if (obj != null && obj.GetValue("powerWatts", StringComparison.OrdinalIgnoreCase) != null)
        {
            type = typeof(Machine1);
            return true;
        }
        type = null;
        return false;
    }
}

And finally deserialize your JSON string as follows:

var settings = new JsonSerializerSettings
{
    Converters = { new Machine1ListConverter<IMachineInfo>() },
};
var list = JsonConvert.DeserializeObject<List<IMachineInfo>>(jsonString, settings);

Of if you want to deserialize to a concrete List<Machine1> do:

var settings = new JsonSerializerSettings
{
    Converters = { new Machine1ListConverter<Machine1>() },
};
var list = JsonConvert.DeserializeObject<List<Machine1>>(jsonString, settings);

Notes:

  • The converter needs to be applied to the overall collection instead of the collection items because JsonConverter.ReadJson has no ability to skip the token currently being read and prevent its return value from being added to the containing object.

  • To deserialize only the items of type Machine2 you could similarly create Machine2ListConverter in which TryInferItemType() checks for the presence of dbm.

  • To deserialize you are calling

    JsonConvert.DeserializeObject<List<Machine1>>(@"C/Path/jsonfile", settings);
    

    But JsonConvert.DeserializeObject Method (String, JsonSerializerSettings) deserializes a JSON string, not a named file. To deserialize from a file see Deserialize JSON from a file.

Demo fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340