0

Given the following JSON:

[
  {
    "ticker": "AAPL",
    "name": "Apple Inc.",
    "market": "STOCKS",
    "locale": "US",
    "currency": "USD",
    "active": true,
    "primaryExch": "NGS",
    "type": "cs",
    "codes": {
      "cik": "0000320193",
      "figiuid": "EQ0010169500001000",
      "scfigi": "BBG001S5N8V8",
      "cfigi": "BBG000B9XRY4",
      "figi": "BBG000B9Y5X2"
    },
    "updated": "2019-01-15T05:21:28.437Z",
    "url": "https://api.polygon.io/v2/reference/tickers/AAPL"
  },
  {
    "ticker": "$AEDAUD",
    "name": "United Arab Emirates dirham - Australian dollar",
    "market": "FX",
    "locale": "G",
    "currency": "AUD",
    "active": true,
    "primaryExch": "FX",
    "updated": "2019-01-25T00:00:00.000Z",
    "attrs": {
      "currencyName": "Australian dollar,",
      "currency": "AUD,",
      "baseName": "United Arab Emirates dirham,",
      "base": "AED"
    },
    "url": "https://api.polygon.io/v2/tickers/$AEDAUD"
  },
  { /* another stock */ },
  { /* another stock */ },
  { /* another FX*/ },
  { /* another stock */ },
  { /* another FX*/ }
  /* and so forth */
]

I wish to deserialize into a common list of type List<Ticker>:

class Ticker
{
   string Ticker {get; set;}
   string Name {get; set;}
   MarketEnum Market {get; set;}
   LocaleEnum Locale {get; set;}
   CurrencyEnum Currency {get; set;}
   bool Active {get; set;}
   PrimaryExchEnum PrimaryExch {get; set;}
   DateTimeOffset Updated {get; set;}
}

class Stock : Ticker
{
  int Type {get; set;}
  string CIK {get; set;}
  string FIGIUID {get; set;}
  string SCFIGI {get; set;}
  string CFIGI {get; set;}
  string FIGI {get; set;}
}

class ForeignExchange : Ticker
{
   BaseCurrencyEnum BaseCurrency {get; set;}
}

I have the following code:

class TickerConvertor : CustomCreationConverter<Ticker>
{
    public override Ticker Create(Type objectType)
    {
        throw new NotImplementedException();
    }

    public Ticker Create(Type objectType, JObject jObject)
    {
        var type = (PrimaryExch)(int)jObject.Property("PrimaryExch");

        if (type == PrimaryExch.FX)
            return new ForeignExchange();
        else
            return new Stock();
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        
        JObject jObject = JObject.Load(reader); // Load JObject from stream 
        var target = Create(objectType, jObject); // Create target object based on JObject 
        serializer.Populate(jObject.CreateReader(), target); // Populate the object properties 

        return target;
    }
}

But I am unsure how I would deal with flattening of the codes sub-structure for the Ticker type, and attrs sub-structure for the ForeignExchange type.

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
morleyc
  • 2,169
  • 10
  • 48
  • 108

1 Answers1

1

To handle both the instantiation of the correct Ticker subclass and the flattening of the sub-structure from the JSON, you can implement a custom JsonConverter like this:

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

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject obj = JObject.Load(reader);

        Ticker ticker;
        JObject childObj;
        if (obj["market"].ToObject<MarketEnum>(serializer) == MarketEnum.FX)
        {
            ticker = new ForeignExchange();
            childObj = (JObject)obj["attrs"];
        }
        else
        {
            ticker = new Stock();
            childObj = (JObject)obj["codes"];
        }

        // populate common properties from the main object
        serializer.Populate(obj.CreateReader(), ticker); 

        // also populate from the selected child object
        serializer.Populate(childObj.CreateReader(), ticker); 

        return ticker;
    }

    public override bool CanWrite
    {
        get { return false; }
    }

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

To use the converter, you can either mark the Ticker class with [JsonConverter(typeof(TickerConverter))] or pass the converter to JsonConvert.DeserializeObject().

But wait, some other changes are needed in your model classes before things will work:

  1. All of your properties need to be public.
  2. You can't have a property name be the same as the class that it is in. In your case the offending property is Ticker. You can either rename the class (e.g. BaseTicker) or rename the property (e.g. Symbol). But if you rename the property you will need to mark it with [JsonProperty("ticker")] to preserve the mapping to the JSON.
  3. In your Stock class the Type property is declared as int but in the JSON it is a string. This will cause an error during deserialization. The property needs to be declared as string.
  4. You have defined various enum properties for many of the properties which are strings in the JSON. You will get a conversion error during deserialization unless you use a StringEnumConverter with each enum. You can do this by marking either the properties in your classes or the enums themselves with [JsonConverter(typeof(StringEnumConverter))]. I would opt for the latter. But one other thing to consider: the StringEnumConverter that ships with Json.Net is very strict. If a value appears in the JSON for which there is not a corresponding value defined in the enum, the converter will throw an error. So you need to make sure that each enum has a complete set of all the possible values you might encounter for it. If you want to make it more tolerant, you can instead use the TolerantEnumConverter shown in How can I ignore unknown enum values during json deserialization?
  5. The name of the BaseCurrency property in the ForeignExchange class does not match the JSON. In the JSON it is called base. So you will need to mark this property with [JsonProperty("base")].

With the above changes made, the classes and enums should look something like this:

[JsonConverter(typeof(TickerConverter))]
class Ticker
{
    [JsonProperty("ticker")]
    public string Symbol { get; set; }
    public string Name { get; set; }
    public MarketEnum Market { get; set; }
    public LocaleEnum Locale { get; set; }
    public CurrencyEnum Currency { get; set; }
    public bool Active { get; set; }
    public PrimaryExchEnum PrimaryExch { get; set; }
    public DateTimeOffset Updated { get; set; }
}

class Stock : Ticker
{
    public string Type { get; set; }
    public string CIK { get; set; }
    public string FIGIUID { get; set; }
    public string SCFIGI { get; set; }
    public string CFIGI { get; set; }
    public string FIGI { get; set; }
}

class ForeignExchange : Ticker
{
    [JsonProperty("base")]
    public BaseCurrencyEnum BaseCurrency { get; set; }
}

[JsonConverter(typeof(StringEnumConverter))]
enum MarketEnum { STOCKS, FX }

[JsonConverter(typeof(StringEnumConverter))]
enum LocaleEnum { US, G }

[JsonConverter(typeof(StringEnumConverter))]
enum CurrencyEnum { USD, AUD, EUR }

[JsonConverter(typeof(StringEnumConverter))]
enum PrimaryExchEnum { NGS, FX }

[JsonConverter(typeof(StringEnumConverter))]
enum BaseCurrencyEnum { AED }

Here is a working demo: https://dotnetfiddle.net/0WQLHT

Brian Rogers
  • 125,747
  • 31
  • 299
  • 300