Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to deserialize collection with different types?

I have a JSON feed that looks like this (I removed some fields that aren't necessary for this example):

{
  "total_count": 2,
  "num_pages": 1,
  "current_page": 1,
  "balance": {
    "amount": "0.00001199",
    "currency": "BTC"
  },
  "transactions": [
    {
      "transaction": {
        "id": "5018f833f8182b129c00002f",
        "created_at": "2012-08-01T02:34:43-07:00",
        "sender": {
          "id": "5011f33df8182b142400000e",
          "name": "User Two",
          "email": "[email protected]"
        },
        "recipient": {
          "id": "5011f33df8182b142400000a",
          "name": "User One",
          "email": "[email protected]"
        }
      }
    },
    {
      "transaction": {
        "id": "5018f833f8182b129c00002e",
        "created_at": "2012-08-01T02:36:43-07:00",
        "hsh": "9d6a7d1112c3db9de5315b421a5153d71413f5f752aff75bf504b77df4e646a3",
        "sender": {
          "id": "5011f33df8182b142400000e",
          "name": "User Two",
          "email": "[email protected]"
        },
        "recipient_address": "37muSN5ZrukVTvyVh3mT5Zc5ew9L9CBare"
      }
    }
 ]
}

There are two types of transactions in this feed: internal transactions that have a recipient, and external transactions that have a hsh and recipient_address.

I created the following classes to accomodate this structure:

UML

So we have a base class for all paged results (PagedResult) with a specific implementation for transactions (TransactionPagedResult). This result has a collection containing 0..* transactions (abstract class Transaction). They're not of the type Transaction though, but of type InternalTransaction or ExternalTransaction which are implementations of Transaction.

My question is how I can let JSON.NET handle this. I want JSON.NET to see whether the current transaction it's parsing is an InternalTransaction or an ExternalTransaction, and add the according type to the IEnumerable<Transaction> collection in TransactionPagedResult.

I created my own JsonConverter that I added as a property to the IEnumerable<Transaction> with the [JsonConverter(typeof(TransactionCreationConverter))] attribute, but this didn't work, I get the following error:

Additional information: Error reading JObject from JsonReader. Current JsonReader item is not an object: StartArray. Path 'transactions', line 1, position 218.

I understand this is because JSON.NET tries to deserialize the whole collection, but I want it to deserialize each object inside the collection one by one.

Anyone?

like image 307
Leon Cullens Avatar asked Jun 15 '14 12:06

Leon Cullens


1 Answers

Your question is essentially a duplicate of this one, and the solution is the same. You need a JsonConverter to instantiate the correct object. However, there are a couple of differences that I see.

If you look at the converter implementation from the other answer, you can see that it looks for a boolean flag in the JSON to determine the type to instantiate. In your case, there is not such a flag, so you'd need to use the existence or absence of a field to make this determination. Also, your list of transactions in the JSON is actually a list of objects that contain transactions, so the converter needs to account for that as well.

With these changes, your converter should look something like this:

public class TransactionConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return typeof(Transaction).IsAssignableFrom(objectType);
    }

    public override object ReadJson(JsonReader reader, 
        Type objectType, object existingValue, JsonSerializer serializer)
    {
        JToken transaction = JToken.Load(reader)["transaction"];
        if (transaction["recipient"] != null)
        {
            return transaction.ToObject<InternalTransaction>();
        }
        else
        {
            return transaction.ToObject<ExternalTransaction>();
        }
    }

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

assuming that your classes are defined like this:

class TransactionPagedResult
{
    [JsonProperty(ItemConverterType=typeof(TransactionConverter))]
    public IEnumerable<Transaction> Transactions { get; set; }
}

class Transaction
{
    public string Id { get; set; }
    [JsonProperty("created_at")]
    public DateTime CreatedAt { get; set; }
}

class InternalTransaction : Transaction
{
    public User Recipient { get; set; }
}

class ExternalTransaction : Transaction
{
    public string Hsh { get; set; }
    [JsonProperty("recipient_address")]
    public string RecipientAddress { get; set; }
}

class User
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

Also, to answer the last part of your question, if you decorate your list with a [JsonConverter] attribute, the converter is expected to handle the entire list. To handle the individual items, you need to use [JsonProperty(ItemConverterType=typeof(TransactionConverter))] on the list instead. I've edited the class definitions above to make this clear.

like image 108
Brian Rogers Avatar answered Oct 08 '22 01:10

Brian Rogers