Morteza
Morteza

Reputation: 41

Deserialize a json object with different structure and same name

I write an app that gets IMDb movie information by scraping movie page source. Some of the movie data in page source are in JSON format with movie schema from "Schema.org".

{
  "@context": "http://schema.org",
  "@type": "Movie",
  "url": "/title/tt7131622/",
  "name": "Once Upon a Time... in Hollywood",
  "genre": [
    "Comedy",
    "Drama"
  ],
  "actor": [
    {
      "@type": "Person",
      "url": "/name/nm0000138/",
      "name": "Leonardo DiCaprio"
    },
    {
      "@type": "Person",
      "url": "/name/nm0000093/",
      "name": "Brad Pitt"
    },
    {
      "@type": "Person",
      "url": "/name/nm3053338/",
      "name": "Margot Robbie"
    },
    {
      "@type": "Person",
      "url": "/name/nm0386472/",
      "name": "Emile Hirsch"
    }
  ],
  "director": {
    "@type": "Person",
    "url": "/name/nm0000233/",
    "name": "Quentin Tarantino"
  },
  "creator": [
    {
      "@type": "Person",
      "url": "/name/nm0000233/",
      "name": "Quentin Tarantino"
    },
    {
      "@type": "Organization",
      "url": "/company/co0050868/"
    },
    {
      "@type": "Organization",
      "url": "/company/co0452101/"
    },
    {
      "@type": "Organization",
      "url": "/company/co0159772/"
    }
}

I made a "Movie" class to deserialize the JSON object. There is a property Person class with the name "Director".

internal class ImdbJsonMovie
    {
        public string Url { get; set; }
        public string Name { get; set; }
        public string Image { get; set; }
        public List<string> Genre { get; set; }
        public List<ImdbJsonPerson> Actor { get; set; }
        public ImdbJsonPerson Director { get; set; }
        //public string[] Creator { get; set; }
    }

It's OK. But the problem is some movies such as "The Matrix" have more than one director.

{
  "@context": "http://schema.org",
  "@type": "Movie",
  "url": "/title/tt0133093/",
  "name": "The Matrix",
  "genre": [
    "Action",
    "Sci-Fi"
  ],
  "actor": [
    {
      "@type": "Person",
      "url": "/name/nm0000206/",
      "name": "Keanu Reeves"
    },
    {
      "@type": "Person",
      "url": "/name/nm0000401/",
      "name": "Laurence Fishburne"
    },
    {
      "@type": "Person",
      "url": "/name/nm0005251/",
      "name": "Carrie-Anne Moss"
    },
    {
      "@type": "Person",
      "url": "/name/nm0915989/",
      "name": "Hugo Weaving"
    }
  ],
  "director": [
    {
      "@type": "Person",
      "url": "/name/nm0905154/",
      "name": "Lana Wachowski"
    },
    {
      "@type": "Person",
      "url": "/name/nm0905152/",
      "name": "Lilly Wachowski"
    }
  ],
  "creator": [
    {
      "@type": "Person",
      "url": "/name/nm0905152/",
      "name": "Lilly Wachowski"
    },
    {
      "@type": "Person",
      "url": "/name/nm0905154/",
      "name": "Lana Wachowski"
    },
    {
      "@type": "Organization",
      "url": "/company/co0002663/"
    },
    {
      "@type": "Organization",
      "url": "/company/co0108864/"
    },
    {
      "@type": "Organization",
      "url": "/company/co0060075/"
    },
    {
      "@type": "Organization",
      "url": "/company/co0019968/"
    },
    {
      "@type": "Organization",
      "url": "/company/co0070636/"
    }
}

So it must be List<Person>.

internal class ImdbJsonMovie
    {
        public string Url { get; set; }
        public string Name { get; set; }
        public string Image { get; set; }
        public List<string> Genre { get; set; }
        public List<ImdbJsonPerson> Actor { get; set; }
        public List<ImdbJsonPerson> Director { get; set; }
        //public string[] Creator { get; set; }
    }

Another problem is how to deserialize creator property that is made by the Person class and Organization class.

So the question is "How to deserialize this complex JSON object?"

Thank you

Upvotes: 1

Views: 1485

Answers (2)

Morteza
Morteza

Reputation: 41

Thank you @Piotr. It completely worked. because your first part of the answer was not correct for me, I rewrite your response as an answer.

as you said the correct answer was in this link.

https://stackoverflow.com/questions/18994685/how-to-handle-both-a-single-item-and-an-array-for-the-same-property-using-json-n

So I made this class.

class JsonConverter<T> : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(List<T>);
    }

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

        if (token.Type == JTokenType.Array)
        {
            return token.ToObject<List<T>>();
        }
        return new List<T> { token.ToObject<T>() };
    }

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

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

and changed my Movie Class to this.

internal class ImdbJsonMovie
{
    public string Url { get; set; }

    [JsonProperty("name")]
    public string Name { get; set; }

    [JsonProperty("image")]
    public string Image { get; set; }

    [JsonProperty("genre")]
    [JsonConverter(typeof(JsonConverter<string>))]
    public List<string> Genre { get; set; }

    [JsonProperty("contentRating")]
    public string ContentRating { get; set; }

    [JsonProperty("actor")]
    [JsonConverter(typeof(JsonConverter<ImdbJsonTypeEnum>))]
    public List<ImdbJsonTypeEnum> Actor { get; set; }

    [JsonProperty("director")] 
    [JsonConverter(typeof(JsonConverter<ImdbJsonTypeEnum>))]
    public List<ImdbJsonTypeEnum> Director { get; set; }

    [JsonProperty("creator")]
    [JsonConverter(typeof(JsonConverter<ImdbJsonTypeEnum>))]
    public List<ImdbJsonTypeEnum> Creator { get; set; }
}

and this Enum

public class ImdbJsonTypeEnum
{
    [JsonProperty("@type")]
    public TypeEnum Type { get; set; }

    [JsonProperty("url")]
    public string Url { get; set; }

    [JsonProperty("name")]
    public string Name { get; set; }

    public enum TypeEnum
    {
        Organization,
        Person
    };
}

It worked for one director and multi director movies.

Thank you

Upvotes: 2

Piotr
Piotr

Reputation: 1197

Did you try: https://app.quicktype.io/?l=csharp? It can generate model in C# for you, which is very good begining for further changes (if the Schema has to be different according to different json responses)

I did enter your JSON and the model created is following:

namespace QuickType
{
    using System;
    using System.Collections.Generic;

    using System.Globalization;
    using Newtonsoft.Json;
    using Newtonsoft.Json.Converters;

    public partial class Movies
    {
        [JsonProperty("@context")]
        public Uri Context { get; set; }

        [JsonProperty("@type")]
        public string Type { get; set; }

        [JsonProperty("url")]
        public string Url { get; set; }

        [JsonProperty("name")]
        public string Name { get; set; }

        [JsonProperty("genre")]
        public List<string> Genre { get; set; }

        [JsonProperty("actor")]
        public List<Tor> Actor { get; set; }

        [JsonProperty("director")]
        public List<Tor> Director { get; set; }

        [JsonProperty("creator")]
        public List<Tor> Creator { get; set; }
    }

    public partial class Tor
    {
        [JsonProperty("@type")]
        public TypeEnum Type { get; set; }

        [JsonProperty("url")]
        public string Url { get; set; }

        [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)]
        public string Name { get; set; }
    }

    public enum TypeEnum { Organization, Person };

    internal static class Converter
    {
        public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
        {
            MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
            DateParseHandling = DateParseHandling.None,
            Converters =
            {
                TypeEnumConverter.Singleton,
                new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }
            },
        };
    }

    internal class TypeEnumConverter : JsonConverter
    {
        public override bool CanConvert(Type t) => t == typeof(TypeEnum) || t == typeof(TypeEnum?);

        public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer)
        {
            if (reader.TokenType == JsonToken.Null) return null;
            var value = serializer.Deserialize<string>(reader);
            switch (value)
            {
                case "Organization":
                    return TypeEnum.Organization;
                case "Person":
                    return TypeEnum.Person;
            }
            throw new Exception("Cannot unmarshal type TypeEnum");
        }

        public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer)
        {
            if (untypedValue == null)
            {
                serializer.Serialize(writer, null);
                return;
            }
            var value = (TypeEnum)untypedValue;
            switch (value)
            {
                case TypeEnum.Organization:
                    serializer.Serialize(writer, "Organization");
                    return;
                case TypeEnum.Person:
                    serializer.Serialize(writer, "Person");
                    return;
            }
            throw new Exception("Cannot marshal type TypeEnum");
        }

        public static readonly TypeEnumConverter Singleton = new TypeEnumConverter();
    }
}

[Update]

As for problems with sometime single, sometime array thing --> look here: How to handle both a single item and an array for the same property using JSON.net

Upvotes: 3

Related Questions