Search code examples
jsonasp.net-mvcxamarin.formsjson.net

c# JsonConvert.DeserializeObject<List<Movie>>(content) doesn't work if return JSON is nested


I'm new to xamarin forms. i'm using a third party api to retrieve a list of movies based on search text of the title. The third party movie api I'm using is below

[movie api][1]
http://www.omdbapi.com/?apikey==*******&s=man

This returns JSON data as below

{"Search":[{"Title":"Iron Man","Year":"2008","imdbID":"tt0371746","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMTczNTI2ODUwOF5BMl5BanBnXkFtZTcwMTU0NTIzMw@@._V1_SX300.jpg"},{"Title":"Iron Man 3","Year":"2013","imdbID":"tt1300854","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMjE5MzcyNjk1M15BMl5BanBnXkFtZTcwMjQ4MjcxOQ@@._V1_SX300.jpg"},{"Title":"Iron Man 2","Year":"2010","imdbID":"tt1228705","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMTM0MDgwNjMyMl5BMl5BanBnXkFtZTcwNTg3NzAzMw@@._V1_SX300.jpg"},{"Title":"Man of Steel","Year":"2013","imdbID":"tt0770828","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMTk5ODk1NDkxMF5BMl5BanBnXkFtZTcwNTA5OTY0OQ@@._V1_SX300.jpg"},{"Title":"Spider-Man","Year":"2002","imdbID":"tt0145487","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BZDEyN2NhMjgtMjdhNi00MmNlLWE5YTgtZGE4MzNjMTRlMGEwXkEyXkFqcGdeQXVyNDUyOTg3Njg@._V1_SX300.jpg"},{"Title":"Ant-Man","Year":"2015","imdbID":"tt0478970","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMjM2NTQ5Mzc2M15BMl5BanBnXkFtZTgwNTcxMDI2NTE@._V1_SX300.jpg"},{"Title":"The Amazing Spider-Man","Year":"2012","imdbID":"tt0948470","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMjMyOTM4MDMxNV5BMl5BanBnXkFtZTcwNjIyNzExOA@@._V1_SX300.jpg"},{"Title":"Spider-Man 2","Year":"2004","imdbID":"tt0316654","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMzY2ODk4NmUtOTVmNi00ZTdkLTlmOWYtMmE2OWVhNTU2OTVkXkEyXkFqcGdeQXVyMTQxNzMzNDI@._V1_SX300.jpg"},{"Title":"Spider-Man: Homecoming","Year":"2017","imdbID":"tt2250912","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BNTk4ODQ1MzgzNl5BMl5BanBnXkFtZTgwMTMyMzM4MTI@._V1_SX300.jpg"},{"Title":"Spider-Man 3","Year":"2007","imdbID":"tt0413300","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BYTk3MDljOWQtNGI2My00OTEzLTlhYjQtOTQ4ODM2MzUwY2IwXkEyXkFqcGdeQXVyNTIzOTk5ODM@._V1_SX300.jpg"}],"totalResults":"10750","Response":"True"}

with three parts in the JSON data -"Search","totalResult" and "Response"

I need to access the data in the "Search" section and deserialised that to my POCO class Movie. How should I do it?

My POCO class for Movie is below

            using Newtonsoft.Json;
        using System;
        using System.Collections.Generic;
        using System.Text;

        namespace NetflixRouletteApp.Models
        {
           public class Movie
            {
    
                [JsonProperty("Title")]
                public string Title { get; set; }

                [JsonProperty("Year")]
                public int Year { get; set; }

                [JsonProperty("Released")]
                public DateTime Released { get; set; }


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

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

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

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

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

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

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

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

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

                [JsonProperty("Awards")]
                public string Awards { get; set; }
                [JsonProperty("Poster")]
                public string Poster { get; set; }

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

            }

        }

MovieService.cs is

            using NetflixRouletteApp.Models;
        using Newtonsoft.Json;
        using System;
        using System.Collections.Generic;
        using System.Linq;
        using System.Net;
        using System.Net.Http;
        using System.Reflection;
        using System.Text;
        using System.Threading.Tasks;
        using RestSharp;
        using RestSharp.Authenticators;
        using System.Threading;
        using System.Net.Http.Headers;
        using Newtonsoft.Json.Linq;

        namespace NetflixRouletteApp.Services
        {
            public class MovieService
            {
                public static readonly int MinSearchLength = 3;
                private const string Url = "http://www.omdbapi.com/?apikey=d09e0406";

                private HttpClient _client = new HttpClient();

                public static Exception CreateExceptionFromResponseErrors(HttpResponseMessage response)
                {
                    var httpErrorObject = response.Content.ReadAsStringAsync().Result;

                    // Create an anonymous object to use as the template for deserialization:
                    var anonymousErrorObject =
                        new { message = "", ModelState = new Dictionary<string, string[]>() };

                    // Deserialize:
                    var deserializedErrorObject =
                        JsonConvert.DeserializeAnonymousType(httpErrorObject, anonymousErrorObject);

                    // Now wrap into an exception which best fullfills the needs of your application:
                    var ex = new Exception();

                    // Sometimes, there may be Model Errors:
                    if (deserializedErrorObject.ModelState != null)
                    {
                        var errors =
                            deserializedErrorObject.ModelState
                                                    .Select(kvp => string.Join(". ", kvp.Value));
                        for (int i = 0; i < errors.Count(); i++)
                        {
                            // Wrap the errors up into the base Exception.Data Dictionary:
                            ex.Data.Add(i, errors.ElementAt(i));
                        }
                    }
                    // Othertimes, there may not be Model Errors:
                    else
                    {
                        var error =
                            JsonConvert.DeserializeObject<Dictionary<string, string>>(httpErrorObject);
                        foreach (var kvp in error)
                        {
                            // Wrap the errors up into the base Exception.Data Dictionary:
                            ex.Data.Add(kvp.Key, kvp.Value);
                        }
                    }
                    return ex;
                }
                public async Task<IEnumerable<Movie>> FindMoviesByActor(string title)
                {
       
                    _client.DefaultRequestHeaders.Accept.Add(
                            new MediaTypeWithQualityHeaderValue("application/json"));

                    HttpResponseMessage response = await _client.GetAsync($"{Url}&s={title}");
                    response.EnsureSuccessStatusCode();
                    try
                    {

                        if (response.StatusCode == HttpStatusCode.NotFound)
                            return Enumerable.Empty<Movie>();
      
                        var content =  response.Content.ReadAsStringAsync().Result;
          
                        List<Movie> movies = JsonConvert.DeserializeObject<List<Movie>>(content);
                        movies.ForEach(Console.WriteLine);
                        return movies;
                    }

                    catch {
                        // Unwrap the response and throw as an Api Exception:
                        var ex = CreateExceptionFromResponseErrors(response);
                        throw ex;

                    }

                  }


                public async Task<Movie> GetMovie(string title)
                {
                    var response = await _client.GetAsync($"{Url}&t={title}");
                    if (response.StatusCode == HttpStatusCode.NotFound)
                        return null;

                    var content = await response.Content.ReadAsStringAsync();
                    return JsonConvert.DeserializeObject<Movie>(content);
                }
            }
            }

MoviePage.Xaml

        <? xml version = "1.0" encoding = "utf-8" ?>

                       < ContentPage xmlns = "http://xamarin.com/schemas/2014/forms"

                                 xmlns: x = "http://schemas.microsoft.com/winfx/2009/xaml"

                                 x: Class = "NetflixRouletteApp.Views.MoviesPage" >

                         < StackLayout >


                             < SearchBar x: Name = "searchBar"

                               Placeholder = "Enter value..."

                               SearchCommand = "{Binding SearchButtonPressed}"

                               SearchCommandParameter = "{Binding Source={x:Reference searchBar}, Path=Text}" />

                            < !--< ActivityIndicator IsRunning = "{Binding IsSearching}" /> -->

                              < Frame x: Name = "notFound" Padding = "20" HasShadow = "False" IsVisible = "False" >
      
                                          < Label Text = "No movies found matching your search." TextColor = "Gray" ></ Label >
         
                                         </ Frame >
         
                                         < ListView x: Name = "moviesListView" ItemSelected = "OnMovieSelected" >
             
                                                 < ListView.ItemTemplate >
             
                                                     < DataTemplate >
             
                                                         < ImageCell ImageSource = "{Binding Poster}"

                                          Text = "{Binding Title}"

                                            Detail = "{Binding Year}" ></ ImageCell >

                                    </ DataTemplate >

                                </ ListView.ItemTemplate >

                            </ ListView >

                        </ StackLayout >

                    </ ContentPage >

MoviePage.xaml.cs

    using NetflixRouletteApp.Models;
using NetflixRouletteApp.Services;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;

namespace NetflixRouletteApp.Views
{
    [XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class MoviesPage : ContentPage
    {
        private MovieService _movieService = new MovieService();
        // Note that IsSearching is a bindable property. This is required for 
        // binding ActivityIndicator's IsRunning property. If we do not define 
        // IsSearching as a bindable property, its initial value (false) will 
        // be used to set ActivityIndicator.IsRunning. Later when we change
        // the value of IsSearching, ActivityIndicator will be unaware of this. 
        // So, we need to implement it as a bindable property. 

        //private BindableProperty IsSearchingProperty = BindableProperty.Create("IsSearching",
        //    typeof(bool), typeof(MoviesPage), false);
        public ICommand SearchButtonPressed { private set; get; }


  
        public MoviesPage()
        {
            BindingContext = this;
            SearchButtonPressed = new Command<string>(HandleSearchPressed);

            InitializeComponent();
        }
        async void HandleSearchPressed(string searchText)
        {
        
            if (searchText == null)
                return;
        
            await FindMovies(title: searchText);

        }

 
        async Task FindMovies(string title)
        {
        
            try
            {

                var movies = await _movieService.FindMoviesByActor(title);
                moviesListView.ItemsSource = movies;
                moviesListView.IsVisible = movies.Any();
                notFound.IsVisible = !moviesListView.IsVisible;
            }
            catch (Exception)
            {
                await DisplayAlert("Error", "Could not retrieve the list of movies.", "OK");
            }
     
        }

        async void OnTextChanged(object sender, TextChangedEventArgs e)
        {
     
            if (e.NewTextValue == null)
                return;
     
            await FindMovies(title: e.NewTextValue);

        }
        async void OnMovieSelected(object sender, SelectedItemChangedEventArgs e)
        {
            if (e.SelectedItem == null)
                return;

            var movie = e.SelectedItem as Movie;

            moviesListView.SelectedItem = null;

            await Navigation.PushAsync(new MovieDetailsPage(movie));
        }

    }
}

Solution

  • go to json2csharp.com and paste your json, it will generate this C# class

    public class Movie    {
        public string Title { get; set; } 
        public string Year { get; set; } 
        public string imdbID { get; set; } 
        public string Type { get; set; } 
        public string Poster { get; set; } 
    }
    
    public class Root    {
        public List<Movie> Search { get; set; } 
        public string totalResults { get; set; } 
        public string Response { get; set; } 
    }
    

    then deserialize the response in your service layer

    Root root = JsonConvert.DeserializeObject<Root>(content);
    List<Movie> Movies = root.Search;