Search Unity

How to Get Reference to API and Build List from It

Discussion in 'Scripting' started by Mashimaro7, Aug 25, 2021.

  1. Mashimaro7

    Mashimaro7

    Joined:
    Apr 10, 2020
    Posts:
    727
    I'm kinda struggling with APIs, I'm making an app that uses the SportsMonks API to get which teams are currently playing, upcoming games, etc... I finally figured out how to pull data from the API, but I'm very confused about how you're supposed to say, pull all currently playing teams and put them in to a list.

    https://docs.sportmonks.com/football/endpoint-overview/leagues/get-all-leagues

    This is the API for getting all leagues. I'm using a for loop to get all the leagues, but here's my script...

    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. using UnityEngine.Networking;
    6. using SimpleJSON;
    7.  
    8. [Serializable]
    9. public class League
    10. {
    11.     public string name;
    12. }
    13.  
    14. public class SportsAPI : MonoBehaviour
    15. {
    16.     [SerializeField] string apiToken;
    17.     [SerializeField] string apiURL;
    18.     public List<League> leagues;
    19.  
    20.     private void Start()
    21.     {
    22.         RequestAPI();
    23.     }
    24.  
    25.     public void RequestAPI()
    26.     {
    27.        
    28.         StartCoroutine(WaitForServerResponse());
    29.     }
    30.  
    31.     IEnumerator WaitForServerResponse()
    32.     {
    33.         using (UnityWebRequest req = UnityWebRequest.Get(apiURL + apiToken))
    34.         {
    35.  
    36.             yield return req.SendWebRequest();
    37.  
    38.             string[] pages = apiURL.Split('/');
    39.             int page = pages.Length - 1;
    40.  
    41.             switch (req.result)
    42.             {
    43.                 case UnityWebRequest.Result.ConnectionError:
    44.                 case UnityWebRequest.Result.DataProcessingError:
    45.                     Debug.LogError(pages[page] + ": Error: " + req.error);
    46.                     break;
    47.                 case UnityWebRequest.Result.ProtocolError:
    48.                     Debug.LogError(pages[page] + ": HTTP Error: " + req.error);
    49.                     break;
    50.                 case UnityWebRequest.Result.Success:
    51.                     print(req.downloadHandler.text);
    52.                     JSONNode node = JSON.Parse(req.downloadHandler.text);
    53.                    
    54.  
    55.                     for (int i = 0; i < leagues.Count; i++)
    56.                     {
    57.                        leagues[i].name = node["data"][i]["id"];
    58.                     }
    59.  
    60.                     break;
    61.             }
    62.         }
    63.  
    64.     }
    65. }
    66.  
    So, do I have to take these values that I get for the "id" and then pass them through some other section of the API? This is all very confusing for me, should I create a method for "GetCountry()" and "GetGoals()" and whatever else I need? Or is there a more efficient way to do this? Should I put all the values that the "League" returns into my League class and just assign them to node["data"]?

    Thanks.
     
  2. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
  3. Mashimaro7

    Mashimaro7

    Joined:
    Apr 10, 2020
    Posts:
    727
    It was a question on how to access APIs in general. Beyond what I have in that script, I don't really know what the best way to go about it is. How are other APIs accessed, how do people generally collect data from APIs and store that info into classes, I've got it half figured out, but I'm kinda just asking for advice.

    Edit: PS I already contacted the dev asking about basic implementation, they said to contact Unity lol
     
  4. Mashimaro7

    Mashimaro7

    Joined:
    Apr 10, 2020
    Posts:
    727
    Bump can anyone please help? My question isn't about how this particular API works, but what a general method for compiling results from an API is. How do I make classes and put all the data into them? For example if I had teams with a name, color, and sprite for flag, how do I create a class for each element of the array? Should I just make a list of teams? But then how do I know how many elements are in the API to create the appropriate number of teams?
     
  5. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    You might have better luck if you could share some sample data. Does the API return JSON or other structure? I assume you are getting a list of teams? Please provide a table of the data that you plan to use.
     
  6. Vryken

    Vryken

    Joined:
    Jan 23, 2018
    Posts:
    2,106
    I'm guessing what you're trying to avoid is this part in your code...
    Code (CSharp):
    1. leagues[i].name = node["data"][i]["id"];
    ...Where it looks like you're manually stepping through the fields of a JSON object to populate some data.

    You don't actually need to do this - JSON parsers are able to parse a full JSON object into a C# object, rather than needing to enumerate over every field one-by-one.
    I'm not familiar with the JSON library you're currently using, so I'll just stick with Unity's default
    JsonUtility
    here.

    Say you want to pull some user information from an API and get a JSON response that looks like this:
    Code (JavaScript):
    1. {
    2.   username: "cooluser32",
    3.   firstName: "Fred",
    4.   lastName: "Flinstone",
    5.   age: 28,
    6.   isOnline: true,
    7.   favoriteDesserts: [
    8.     "Cake",
    9.     "Ice Cream",
    10.     "Cookies"
    11.   ]
    12. }
    You can create a class in C# to mimic this object structure like so...
    Code (CSharp):
    1. public class User
    2. {
    3.   string username;
    4.   string firstName;
    5.   string lastName;
    6.   int age;
    7.   bool isOnline;
    8.   string[] favoriteDesserts;
    9. }
    ...And then parse all of its data at once like so:
    Code (CSharp):
    1. string jsonData = //some JSON data source containing User object...
    2.  
    3. User user = JsonUtility.FromJson<User>(jsonData);
    The JSON library you're currently using should be able to do the same, and, assuming it can parse a top-level array of JSON data (unlike
    JsonUtility
    ), you can do the same with an array or list of objects like so:
    Code (CSharp):
    1. //Assuming the response is an array of Users.
    2. List<User> users = JSON.Parse(req.downloadHandler.text);
     
    Last edited: Aug 27, 2021
    Mashimaro7 likes this.
  7. Mashimaro7

    Mashimaro7

    Joined:
    Apr 10, 2020
    Posts:
    727
    Sorry, yes it returns JSON(I'm not familiar with APIs, just been learning about them and kinda assumed they all returned in JSON format lol)

    Ahh, this is what was throwing me off! I forgot how arrays were formatted in JSON!

    So I could make a class like that, and it would automatically populate it as long as the variables are the same name?
     
    Last edited: Aug 27, 2021
    Vryken likes this.
  8. Vryken

    Vryken

    Joined:
    Jan 23, 2018
    Posts:
    2,106
    Bingo.
     
    Mashimaro7 likes this.
  9. Mashimaro7

    Mashimaro7

    Joined:
    Apr 10, 2020
    Posts:
    727
    Okay, I am inexperienced with both JSON and APIs haha, so good to know!
    Thanks for the help!
     
  10. Mashimaro7

    Mashimaro7

    Joined:
    Apr 10, 2020
    Posts:
    727
    Hi, sorry I thought I was all set but when I tried it it's not really working. I have JSONUtility, so I was just setting it directly by going
    Code (CSharp):
    1. leagues = JsonUtility.FromJson<List<League>>(req.downloadHandler.text);
    but it's not generating a list at all... I also tried generating a list by checking if it was null and just adding if it was not(Also added the "data" at the start of the JSON), it adds 4 elements, but they're all blank... Here's my API call script along with the League class,
    Code (CSharp):
    1. using System;
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. using UnityEngine.Networking;
    6. using SimpleJSON;
    7.  
    8. [Serializable]
    9. public class League
    10. {
    11.     public int id;
    12.     public bool active;
    13.     public string type;
    14.     public int legacy_id;
    15.     public int country_id;
    16.     public string logo_path;
    17.     public string name;
    18.     public bool is_cup;
    19.     public bool is_friendly;
    20.     public int current_season_id;
    21.     public int current_round_id;
    22.     public int current_stage_id;
    23.     public bool live_standings;
    24.     public bool[] coverage;
    25. }
    26.  
    27. public class SportsAPI : MonoBehaviour
    28. {
    29.     [SerializeField] string apiToken;
    30.     [SerializeField] string apiURL;
    31.     public List<League> leagues;
    32.  
    33.     private void Start()
    34.     {
    35.         RequestAPI();
    36.     }
    37.  
    38.     public void RequestAPI()
    39.     {
    40.         leagues = new List<League>();
    41.         StartCoroutine(WaitForServerResponse());
    42.     }
    43.  
    44.     IEnumerator WaitForServerResponse()
    45.     {
    46.         using (UnityWebRequest req = UnityWebRequest.Get(apiURL + apiToken))
    47.         {
    48.  
    49.             yield return req.SendWebRequest();
    50.  
    51.             string[] pages = apiURL.Split('/');
    52.             int page = pages.Length - 1;
    53.             JSONNode node = null;
    54.             string data = "";
    55.  
    56.             switch (req.result)
    57.             {
    58.                 case UnityWebRequest.Result.ConnectionError:
    59.                 case UnityWebRequest.Result.DataProcessingError:
    60.                     Debug.LogError(pages[page] + ": Error: " + req.error);
    61.                     break;
    62.                 case UnityWebRequest.Result.ProtocolError:
    63.                     Debug.LogError(pages[page] + ": HTTP Error: " + req.error);
    64.                     break;
    65.                 case UnityWebRequest.Result.Success:
    66.                     print(req.downloadHandler.text);
    67.  
    68.                     leagues = JsonUtility.FromJson<List<League>>(req.downloadHandler.text);
    69.                     print((req.downloadHandler.text));
    70.  
    71.                     break;
    72.             }
    73.  
    74.        
    75.  
    76.         }
    77.  
    78.     }
    79. }
    80.  
    And this is what the JSON looks like when printed off,
    Code (CSharp):
    1. {"data":[{"id":271,"active":true,"type":"domestic","legacy_id":43,"country_id":320,"logo_path":"https:\/\/cdn.sportmonks.com\/images\/soccer\/leagues\/271.png","name":"Superliga","is_cup":false,"is_friendly":false,"current_season_id":18334,"current_round_id":246630,"current_stage_id":77453568,"live_standings":true,"coverage":{"predictions":true,"topscorer_goals":true,"topscorer_assists":true,"topscorer_cards":true}},
    2. {"id":501,"active":true,"type":"domestic","legacy_id":66,"country_id":1161,"logo_path":"https:\/\/cdn.sportmonks.com\/images\/soccer\/leagues\/501.png","name":"Premiership","is_cup":false,"is_friendly":false,"current_season_id":18369,"current_round_id":247434,"current_stage_id":77453684,"live_standings":true,"coverage":{"predictions":true,"topscorer_goals":true,"topscorer_assists":true,"topscorer_cards":true}},
    3. {"id":513,"active":true,"type":"domestic","legacy_id":null,"country_id":1161,"logo_path":"https:\/\/cdn.sportmonks.com\/images\/soccer\/leagues\/1\/513.png","name":"Premiership Play-Offs","is_cup":false,"is_friendly":false,"current_season_id":18260,"current_round_id":null,"current_stage_id":null,"live_standings":false,"coverage":{"predictions":true,"topscorer_goals":true,"topscorer_assists":true,"topscorer_cards":true}},
    4. {"id":1659,"active":true,"type":"domestic","legacy_id":null,"country_id":320,"logo_path":"https:\/\/cdn.sportmonks.com\/images\/\/soccer\/leagues\/27\/1659.png","name":"Superliga Play-offs","is_cup":false,"is_friendly":false,"current_season_id":18101,"current_round_id":null,"current_stage_id":null,"live_standings":false,"coverage":{"predictions":true,"topscorer_goals":true,"topscorer_assists":true,"topscorer_cards":true}}],
    5. "meta":{"plans":[{"name":"Football Free Plan","features":"Standard","request_limit":"180,60","sport":"Soccer"}],"sports":
    6. [{"id":1,"name":"Soccer","current":true}],"pagination":{"total":4,"count":4,"per_page":100,"current_page":1,"total_pages":1,"links":{}}}}
    Is it because of the "data" at the start of the JSON? Also, I'm not even sure what all this extra data is at the end or how I access it lol
     
  11. Vryken

    Vryken

    Joined:
    Jan 23, 2018
    Posts:
    2,106
    Yeah, the response JSON from the API here is more than just a list of Leagues.
    Let's first format the JSON response to make it more readable:
    Code (JavaScript):
    1. {
    2.   "data": [
    3.     {
    4.       "id": 271,
    5.       "active": true,
    6.       "type": "domestic",
    7.       "legacy_id": 43,
    8.       "country_id": 320,
    9.       "logo_path": "https://cdn.sportmonks.com/images/soccer/leagues/271.png",
    10.       "name": "Superliga",
    11.       "is_cup": false,
    12.       "is_friendly": false,
    13.       "current_season_id": 18334,
    14.       "current_round_id": 246630,
    15.       "current_stage_id": 77453568,
    16.       "live_standings": true,
    17.       "coverage": {
    18.         "predictions": true,
    19.         "topscorer_goals": true,
    20.         "topscorer_assists": true,
    21.         "topscorer_cards": true
    22.       }
    23.     },
    24.     {
    25.       "id": 501,
    26.       "active": true,
    27.       "type": "domestic",
    28.       "legacy_id": 66,
    29.       "country_id": 1161,
    30.       "logo_path": "https://cdn.sportmonks.com/images/soccer/leagues/501.png",
    31.       "name": "Premiership",
    32.       "is_cup": false,
    33.       "is_friendly": false,
    34.       "current_season_id": 18369,
    35.       "current_round_id": 247434,
    36.       "current_stage_id": 77453684,
    37.       "live_standings": true,
    38.       "coverage": {
    39.         "predictions": true,
    40.         "topscorer_goals": true,
    41.         "topscorer_assists": true,
    42.         "topscorer_cards": true
    43.       }
    44.     },
    45.     {
    46.       "id": 513,
    47.       "active": true,
    48.       "type": "domestic",
    49.       "legacy_id": null,
    50.       "country_id": 1161,
    51.       "logo_path": "https://cdn.sportmonks.com/images/soccer/leagues/1/513.png",
    52.       "name": "Premiership Play-Offs",
    53.       "is_cup": false,
    54.       "is_friendly": false,
    55.       "current_season_id": 18260,
    56.       "current_round_id": null,
    57.       "current_stage_id": null,
    58.       "live_standings": false,
    59.       "coverage": {
    60.         "predictions": true,
    61.         "topscorer_goals": true,
    62.         "topscorer_assists": true,
    63.         "topscorer_cards": true
    64.       }
    65.     },
    66.     {
    67.       "id": 1659,
    68.       "active": true,
    69.       "type": "domestic",
    70.       "legacy_id": null,
    71.       "country_id": 320,
    72.       "logo_path": "https://cdn.sportmonks.com/images//soccer/leagues/27/1659.png",
    73.       "name": "Superliga Play-offs",
    74.       "is_cup": false,
    75.       "is_friendly": false,
    76.       "current_season_id": 18101,
    77.       "current_round_id": null,
    78.       "current_stage_id": null,
    79.       "live_standings": false,
    80.       "coverage": {
    81.         "predictions": true,
    82.         "topscorer_goals": true,
    83.         "topscorer_assists": true,
    84.         "topscorer_cards": true
    85.       }
    86.     }
    87.   ],
    88.   "meta": {
    89.     "plans": [
    90.       {
    91.         "name": "Football Free Plan",
    92.         "features": "Standard",
    93.         "request_limit": "180,60",
    94.         "sport": "Soccer"
    95.       }
    96.     ],
    97.     "sports": [{ "id": 1, "name": "Soccer", "current": true }],
    98.     "pagination": {
    99.       "total": 4,
    100.       "count": 4,
    101.       "per_page": 100,
    102.       "current_page": 1,
    103.       "total_pages": 1,
    104.       "links": {}
    105.     }
    106.   }
    107. }
    In JSON, everything wrapped between curly-braces
    {}
    is an object, so when parsing the data into C#, every JSON object you see will usually be its own C# object.
    So the C# structure for this response would look something like this...
    Code (CSharp):
    1. public class ApiResponse
    2. {
    3.   public League[] data;
    4.   public ApiResponseMeta meta;
    5. }
    6.  
    7. public class ApiResponseMeta
    8. {
    9.   public Plan[] plans;
    10.   public Sport[] sports;
    11.   public Pagination pagination;
    12. }
    13.  
    14. public class Plan
    15. {
    16.   public string name;
    17.   public string features;
    18.   public string request_limit;
    19.   public string sport;
    20. }
    21.  
    22. public class Sport
    23. {
    24.   public int id;
    25.   public string name;
    26.   public bool current;
    27. }
    28.  
    29. public class Pagination
    30. {
    31.   public int total;
    32.   public int count;
    33.   public int per_page;
    34.   public int current_page;
    35.   public int total_pages;
    36.   public PaginationLinks links;
    37. }
    38.  
    39. public class PaginationLinks
    40. {
    41.   //Unknown what this model would look like - it was an empty object in the JSON response.
    42. }
    ...And rather than converting from JSON to a
    League
    array/list, you'd convert from JSON to an
    ApiResponse
    :
    Code (CSharp):
    1. ApiResponse response = JsonUtility.FromJson<ApiResponse>(req.downloadHandler.text);
    2.  
    3. League[] leagues = response.data;
    Now while this will work, maybe you might need to request some data other than Leagues, but currently,
    ApiResponse
    only contains an array of Leagues specifically.
    Rather than needing to rewrite unique API response models for every type of data you want to request, you can utilize generics to customize the type of response data instead like so...
    Code (CSharp):
    1. public class ApiResponse<T>
    2. {
    3.   public T[] data;
    4.   public ApiResponseMeta meta;
    5. }
    ...And then specify the
    <T>
    type when you parse the JSON:
    Code (CSharp):
    1. ApiResponse<League> response = JsonUtility.FromJson<ApiResponse<League>>(req.downloadHandler.text);
    2.  
    3. League[] leagues = response.data;
    Regarding all that extra meta information in the response, you can probably just ignore it if you don't need to do anything with it, and omit it entirely from the
    ApiResponse
    model:
    Code (CSharp):
    1. public class ApiResponse<T>
    2. {
    3.   public T[] data;
    4. }
    Although the pagination details might be needed.

    Typically, when requesting a bunch of data from a web API, instead of returning possibly thousands of items in an array at once, it's usually limited up to a certain amount to save memory & increase response times.
    Because of this, pagination details are included that usually contain another two URLs that you can request to get the next or previous set (or "page") of data.

    The
    per_page
    in the JSON there seems to indicate that the API will only return a maximum of 100 items in the
    data
    array at once.
    If there were any more items to request, the URLs for them would probably exist in the
    links
    object and might look something like this:
    Code (JavaScript):
    1. links {
    2.   next_page: "url/to/next/100/items";
    3.   previous_page: "url/to/previous/100/items";
    4. }
    You'd have to look at the API's documentation to see what it actually looks like though.

    The
    pagination
    info does appear to indicate that there are no more Leagues to request though, so if these 4 Leagues are all you need to work with, you can probably ignore the pagination information as well then.
     
    Last edited: Aug 28, 2021
    Mashimaro7 likes this.
  12. Mashimaro7

    Mashimaro7

    Joined:
    Apr 10, 2020
    Posts:
    727
    Sorry for my late response.

    Thank you! I was able to use this code and pull from the API... I don't fully understand how this works, but I will study it on my own time... One thing, you said I probably didn't need the pagination anyway, but
    Code (CSharp):
    1. public PaginationLinks links;
    Is getting a could not be found error. Is there some namespace I need to put in? For future reference.

    I'll definitely need some more API references, so thank you so much for teaching me how to make it generic as well :) I'll put all this API call stuff into it's own unique class.

    Thanks again

    Edit: Oh, I'm dumb, I just realized I didn't copy the "PaginationLinks" class lol
     
    Last edited: Aug 29, 2021
  13. Vryken

    Vryken

    Joined:
    Jan 23, 2018
    Posts:
    2,106
    No problem.

    PaginationLinks
    is just a custom class I wrote in the above example. You can call it whatever you want, really.
    It's supposed to represent the "links" part inside the "pagination" part at the bottom the JSON response, but because that part in the response is just an empty object, I don't actually know what the C# class for it should look like.

    You would have to look at the API's documentation to see what kind of object structure is returned in the "links" part of the JSON response to model it in a C# class.
     
    Mashimaro7 likes this.
  14. Mashimaro7

    Mashimaro7

    Joined:
    Apr 10, 2020
    Posts:
    727
    Oh okay, also I was going to ask you about something else but fixed it on my own :p

    For anyone who might read this post in the future, my array of bools "coverage" was actually a separate class, not an array of bools. So I just created another class for that and it's assigning all values properly.

    Thanks for all the useful info, @Vryken ! I will study it more, I think I kinda got it figured out now though :)
     
    Vryken likes this.
  15. Mashimaro7

    Mashimaro7

    Joined:
    Apr 10, 2020
    Posts:
    727
    Hi again, sorry to necro a post from 3 weeks ago, I figure it would be better than making a new post. I had an issue with one part of the api. I got some of them working, but I can't get any of the statistics working, here
    https://docs.sportmonks.com/football/tutorials/statistics
    It seems to be returning an array "Stats" of an array "Data" ? How do I go about getting a reference to this? Do I have to make a new coroutine for this and/or a new class?

    This is what my class looks like at the moment, the "Statistics" class is for returning the player statistics. Neither the match stats or the statistics populate, but all the other lists do.

    Code (CSharp):
    1.  
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. using UnityEngine.Networking;
    6.  
    7. public class APICaller : MonoBehaviour
    8. {
    9.  
    10.     public List<League> leagues;
    11.     public List<Country> countries;
    12.     public List<Team> teams;
    13.     public List<LiveScores> scores;
    14.     public List<Season> seasons;
    15.     public List<Continent> continents;
    16.     public List<Statistics> statistics;
    17.     public List<MatchStats> matchStats;
    18.  
    19.  
    20.     static APICaller _api;
    21.  
    22.     public static APICaller api
    23.     {
    24.         get
    25.         {
    26.             return _api;
    27.         }
    28.     }
    29.  
    30.     private void Awake()
    31.     {
    32.         _api = this;
    33.         DontDestroyOnLoad(gameObject);
    34.     }
    35.  
    36.     private void Start()
    37.     {
    38.         RequestAPI("Leagues",leagues);
    39.         RequestAPI("Countries",countries);
    40.         RequestAPI("Teams",teams);
    41.         RequestAPI("Scores",scores);
    42.         RequestAPI("Seasons",seasons);
    43.         RequestAPI("Continents", continents);
    44.         RequestAPI("Statistics", statistics);
    45.         RequestAPI("MatchStats", matchStats);
    46.  
    47.     }
    48.  
    49.     public void RequestAPI<T>(string name,List<T> list)
    50.     {      
    51.         StartCoroutine(WaitForServerResponse(name,list));
    52.  
    53.     }
    54.  
    55.     IEnumerator WaitForServerResponse<T>(string name,List<T> list)
    56.     {
    57.         using (UnityWebRequest req = UnityWebRequest.Get(URLRefs.url.GetURL(name)))
    58.         {
    59.             yield return req.SendWebRequest();
    60.  
    61.             string[] pages = req.url.Split('/');
    62.             int page = pages.Length - 1;
    63.      
    64.  
    65.             switch (req.result)
    66.             {
    67.                 case UnityWebRequest.Result.ConnectionError:
    68.                     break;
    69.                 case UnityWebRequest.Result.DataProcessingError:
    70.                     Debug.LogError(pages[page] + ": Error: " + req.error);
    71.                     break;
    72.                 case UnityWebRequest.Result.ProtocolError:
    73.                     Debug.LogError(pages[page] + ": HTTP Error: " + req.error + " " + name);
    74.                     break;
    75.                 case UnityWebRequest.Result.Success:
    76.  
    77.                     ApiResponse<T> response = JsonUtility.FromJson<ApiResponse<T>>(req.downloadHandler.text);
    78.  
    79.                     print(req.downloadHandler.text);
    80.                     for (int i = 0; i < response.data.Count; i++)
    81.                     {
    82.                        
    83.                         list.Add(response.data[i]);
    84.                     }
    85.                     break;
    86.             }
    87.  
    88.         }
    89.  
    90.     }
    91.  
    92. }
    Do I have to make a list of datas? If so, how do I get this to show up in the inspector?(not necessary, but it'd be useful)
     
  16. Vryken

    Vryken

    Joined:
    Jan 23, 2018
    Posts:
    2,106
    After looking through the API docs you linked, it looks like "stats" is an optional field that can be included in an API response.
    "stats" is not actually an array, but another object that contains an array of "data", and you should be able to just include it as an additional field in your
    ApiResponse
    class:
    Code (CSharp):
    1. public class ApiResponseStats
    2. {
    3.   public object[] data;
    4. }
    5.  
    6. public ApiResponse<T>
    7. {
    8.   public T[] data;
    9.   public ApiResponseMeta meta;
    10.   public ApiResponseStats stats;
    11. }
    You could also implement generics in the same way for
    ApiResponseStats
    that you've done for
    ApiResponse
    if you want to cast the data to a specific type, but that would mean you'd have to do something like this...
    Code (CSharp):
    1. public class ApiResponseStats<T>
    2. {
    3.   public T[] data;
    4. }
    5.  
    6. public ApiResponse<TData, TStats>
    7. {
    8.   public TData[] data;
    9.   public ApiResponseMeta meta;
    10.   public ApiResponseStats<TStats> stats;
    11. }
    ...And that starts becoming a bit cumbersome, especially since "stats" are an optional response field anyway.

    Maybe a better approach for them instead would be to keep "data" as an
    object
    array, and then just add a simple get method that casts the data into a specified type, like so:
    Code (CSharp):
    1. public class ApiResponseStats
    2. {
    3.   public object[] data;
    4.  
    5.   public T[] GetDataAs<T>() => data as T[];
    6. }
    7.  
    8. public ApiResponse<T>
    9. {
    10.   public T[] data;
    11.   public ApiResponseMeta meta;
    12.   public ApiResponseStats stats;
    13. }
    Usage would be something like this:
    Code (CSharp):
    1. public class SomeStatsClass
    2. {
    3.   //etc...
    4. }
    5.  
    6. public class ApiRequester
    7. {
    8.   void Request()
    9.   {
    10.     //etc...
    11.  
    12.     ApiResponse<T> response = JsonUtility.FromJson<ApiResponse<T>>(req.downloadHandler.text);
    13.  
    14.     SomeStatsClass stats = response.stats.GetDataAs<SomeStatsClass>();
    15.   }
    16. }
    Saying all that though, I don't actually know if JSON parsing works with just the plain C#
    object
    type.
    If it doesn't, maybe the
    dynamic
    type might work instead?
    Code (CSharp):
    1. public class ApiResponseStats
    2. {
    3.   public dynamic[] data;
    4.  
    5.   public T[] GetDataAs<T>() => data as T[];
    6. }
    That's something to experiment with.
     
    Last edited: Sep 20, 2021
  17. Mashimaro7

    Mashimaro7

    Joined:
    Apr 10, 2020
    Posts:
    727
    Sorry for the late response. Hmm, I couldn't really get it working...

    I'm unable to assign the list directly, which is why I was running a for loop(some weirdness with coroutines? Not exactly sure, it was letting me add from the list but not assign directly...

    So, I tried this,
    Code (CSharp):
    1. using System.Collections.Generic;
    2.  
    3. public class ApiResponseStats<T>
    4. {
    5.     public List<T> data;
    6.  
    7.     public T[] GetDataAs<T>() => data as T[];
    8. }
    9.  
    10. public class ApiResponse<T>
    11. {
    12.   public List<T> data;
    13. public ApiResponseMeta meta;
    14. public ApiResponseStats<T> stats;
    15. }
    16.  
    Because I couldn't get the one you showed working haha, and tried making a separate method just for these stats classes, but it's getting a null reference exception for those two... Here's my script now, I know it's kinda sloppy with two almost identical coroutines, I was trying to get it working :p

    Code (CSharp):
    1.  
    2. using System.Collections;
    3. using System.Collections.Generic;
    4. using UnityEngine;
    5. using UnityEngine.Networking;
    6.  
    7. public class APICaller : MonoBehaviour
    8. {
    9.  
    10.     public List<League> leagues;
    11.     public List<Country> countries;
    12.     public List<Team> teams;
    13.     public List<LiveScores> scores;
    14.     public List<Season> seasons;
    15.     public List<Continent> continents;
    16.     public List<Statistics> statistics;
    17.     public List<MatchStats> matchStats;
    18.  
    19.  
    20.     static APICaller _api;
    21.  
    22.     public static APICaller api
    23.     {
    24.         get
    25.         {
    26.             return _api;
    27.         }
    28.     }
    29.  
    30.     private void Awake()
    31.     {
    32.         _api = this;
    33.         DontDestroyOnLoad(gameObject);
    34.     }
    35.  
    36.     private void Start()
    37.     {
    38.         RequestAPI("Leagues",leagues);
    39.         RequestAPI("Countries",countries);
    40.         RequestAPI("Teams",teams);
    41.         RequestAPI("Scores",scores);
    42.         RequestAPI("Seasons",seasons);
    43.         RequestAPI("Continents", continents);
    44.         RequestAPIStats("Statistics", statistics);
    45.         RequestAPIStats("MatchStats", matchStats);
    46.  
    47.     }
    48.  
    49.     public void RequestAPI<T>(string name,List<T> list)
    50.     {      
    51.         StartCoroutine(WaitForServerResponse(name,list));
    52.  
    53.     }
    54.  
    55.     public void RequestAPIStats<T>(string name, List<T> list)
    56.     {
    57.         StartCoroutine(WaitForStatsResponse(name, list));
    58.  
    59.     }
    60.  
    61.     IEnumerator WaitForServerResponse<T>(string name,List<T> list)
    62.     {
    63.         using (UnityWebRequest req = UnityWebRequest.Get(URLRefs.url.GetURL(name)))
    64.         {
    65.             yield return req.SendWebRequest();
    66.  
    67.             string[] pages = req.url.Split('/');
    68.             int page = pages.Length - 1;
    69.      
    70.  
    71.             switch (req.result)
    72.             {
    73.                 case UnityWebRequest.Result.ConnectionError:
    74.                     break;
    75.                 case UnityWebRequest.Result.DataProcessingError:
    76.                     Debug.LogError(pages[page] + ": Error: " + req.error);
    77.                     break;
    78.                 case UnityWebRequest.Result.ProtocolError:
    79.                     Debug.LogError(pages[page] + ": HTTP Error: " + req.error + " " + name);
    80.                     break;
    81.                 case UnityWebRequest.Result.Success:
    82.  
    83.                     ApiResponse<T> response = JsonUtility.FromJson<ApiResponse<T>>(req.downloadHandler.text);
    84.  
    85.                     for (int i = 0; i < response.data.Count; i++)
    86.                     {
    87.                         list.Add(response.data[i]);
    88.                     }
    89.                     break;
    90.             }
    91.  
    92.         }
    93.  
    94.     }
    95.  
    96.     IEnumerator WaitForStatsResponse<T>(string name, List<T> list)
    97.     {
    98.         using (UnityWebRequest req = UnityWebRequest.Get(URLRefs.url.GetURL(name)))
    99.         {
    100.             yield return req.SendWebRequest();
    101.  
    102.             string[] pages = req.url.Split('/');
    103.             int page = pages.Length - 1;
    104.  
    105.  
    106.             switch (req.result)
    107.             {
    108.                 case UnityWebRequest.Result.ConnectionError:
    109.                     break;
    110.                 case UnityWebRequest.Result.DataProcessingError:
    111.                     Debug.LogError(pages[page] + ": Error: " + req.error);
    112.                     break;
    113.                 case UnityWebRequest.Result.ProtocolError:
    114.                     Debug.LogError(pages[page] + ": HTTP Error: " + req.error + " " + name);
    115.                     break;
    116.                 case UnityWebRequest.Result.Success:
    117.  
    118.                     ApiResponse<T> response = JsonUtility.FromJson<ApiResponse<T>>(req.downloadHandler.text);
    119.  
    120.                     for (int i = 0; i < response.stats.data.Count; i++)
    121.                     {
    122.                         list.Add(response.stats.data[i]);
    123.                     }
    124.                     break;
    125.             }
    126.  
    127.         }
    128.  
    129.     }
    130.  
    131. }
     
  18. Vryken

    Vryken

    Joined:
    Jan 23, 2018
    Posts:
    2,106
    Make sure the request URL has the
    include=stats
    query parameter like the example in the docs, otherwise the response won't return the stats, and that's when you'll get a null reference exception when you try to access it:
    Code (JavaScript):
    1. https://soccer.sportmonks.com/api/v2.0/fixtures/16475287?api_token={API_TOKEN}&include=stats
    The stats would be included in the same API response, rather than needing to send a second request after getting the main data first.

    There's also a slight issue with your
    ApiResponse
    model here...
    Code (CSharp):
    1. public class ApiResponse<T>
    2. {
    3.   public List<T> data;
    4.   public ApiResponseMeta meta;
    5.   public ApiResponseStats<T> stats;
    6. }
    ...Where
    data
    and
    stats
    fields are of the same generic type, when they should be two different types.
    You can do this using the example in the previous post:
    Code (CSharp):
    1. public class ApiResponseStats<T>
    2. {
    3.   public T[] data;
    4. }
    5.  
    6. public ApiResponse<TData, TStats>
    7. {
    8.   public TData[] data;
    9.   public ApiResponseMeta meta;
    10.   public ApiResponseStats<TStats> stats;
    11. }
    The generic types also don't need to be called "TData" and "TStats" specifically - you can call them whatever you like.
    Though again, if stats are an optional field you don't always need for every API request, you probably want to use the other example model...
    Code (CSharp):
    1. public class ApiResponseStats
    2. {
    3.   public object[] data;
    4.    
    5.   public T[] GetDataAs<T>() => data as T[];
    6. }
    7.    
    8. public ApiResponse<T>
    9. {
    10.   public T[] data;
    11.   public ApiResponseMeta meta;
    12.   public ApiResponseStats stats;
    13. }
    ...Just so that you don't have to always specify two generic types for
    ApiResponse
    when stats aren't needed.
     
    Mashimaro7 likes this.
  19. Mashimaro7

    Mashimaro7

    Joined:
    Apr 10, 2020
    Posts:
    727
    Sorry again for the lateness, I just got around to working on this, had a crazy week. Thank you for all the help.

    I can't get it working though, if I do it this way,
    Code (CSharp):
    1. public class ApiResponseStats
    2. {
    3.   public object[] data;
    4.  
    5.   public T[] GetDataAs<T>() => data as T[];
    6. }
    7.  
    8. public ApiResponse<T>
    9. {
    10.   public T[] data;
    11.   public ApiResponseMeta meta;
    12.   public ApiResponseStats stats;
    13. }
    It throws an error because ApiResponseStats doesn't declare the generic T. And if I make it declare a generic, then it will have the aforementioned identical generics issue. How do I solve this issue?

    The thing I don't really get is, if I type the URL directly into my browser with the API key(not my key so I can't share it here), it shows all the data starting with
    {"data":{"id":16475287, etc

    So you'd think just assigning it to data would work, but it isn't working... It's assigning every other list just fine,
    https://imgur.com/a/lwlNKnE

    Thanks again for the patience, sorry for being dumb lol, learning APIs feel like learning a whole different language to me and there isn't a lot of info I could find...
     
  20. Mashimaro7

    Mashimaro7

    Joined:
    Apr 10, 2020
    Posts:
    727
  21. Vryken

    Vryken

    Joined:
    Jan 23, 2018
    Posts:
    2,106
    I've only been briefly skimming through this API, but now that I've really taken a deeper look at it, I think you should scrap pretty much everything I've mentioned so far, because I've honestly just made this more confusing than it needs to be. :confused:
    So let's start over.

    The API is returning data in a JSON format.
    In JSON, curly braces
    {}
    denote an object, and square brackets
    []
    denote an array.

    Web APIs typically follow this convention when requesting data:
    • /path/to/resources
      will return all resources in an array of objects
      [ {}, {}, {} ... ]
      .
    • /path/to/resources/1
      will return one resource in an object
      {}
      by an ID (the ID being 1 in this example).
    An example of this is with Leagues:
    In C#, you do still need to model classes after the response data, so that doesn't change:
    Code (CSharp):
    1. public class League
    2. {
    3.   public int id;
    4.   public bool active;
    5.   public string type;
    6.   //etc...
    7. }
    The key-thing about this particular API though seems to be its "includes" feature that is used pretty much everywhere.
    For instance with Leagues, you can include the Country data that is associated with a League like so:
    https://soccer.sportmonks.com/api/v2.0/leagues?include=country

    Which would return a response similar to this:
    Code (JavaScript):
    1. {
    2.   "data": [
    3.     {
    4.       "id": 1,
    5.       "active": true,
    6.       "type": "domestic",
    7.  
    8.       //etc...
    9.  
    10.       "country": {
    11.         "data" : {
    12.           "id": 2,
    13.           "name": "Poland",
    14.  
    15.           //etc...
    16.         }
    17.       }
    18.     }
    19.   }
    20. }
    The format for both the base response data and the "includes" children data is the same - a field named "data" that is either an object
    {}
    or an array
    []
    .
    With that in mind, we can re-create
    ApiResponse
    (Maybe rename it to "ApiData" or something to make a little more sense) to be as simple as this:
    Code (CSharp):
    1. public class ApiData<T>
    2. {
    3.   public T data;
    4. }
    Now if we wanted to include Country data within our League class, all we need to do is add a child
    ApiData
    of type
    Country
    within the League class (you of course need to create a Country class as well):
    Code (CSharp):
    1. public class League
    2. {
    3.   public int id;
    4.   public bool active;
    5.   public string type;
    6.   //etc...
    7.  
    8.   public ApiData<Country> country;
    9. }
    From here, you can add as many "included" children as you need. For example, according to the documentation, Leagues can also include Seasons. If you wanted to add Seasons in addition to Country for League, you'd just do it like so...
    Code (CSharp):
    1. public class League
    2. {
    3.   public int id;
    4.   public bool active;
    5.   public string type;
    6.   //etc...
    7.  
    8.   public ApiData<Country> country;
    9.   public ApiData<Season[]> seasons;
    10. }
    ...And example API requests would be:
    Code (JavaScript):
    1. //Get all Leagues without including Country or Seasons.
    2. https://soccer.sportmonks.com/api/v2.0/leagues
    3.  
    4. //Get all Leagues and include Country.
    5. https://soccer.sportmonks.com/api/v2.0/leagues?include=country
    6.  
    7. //Get all Leagues and include Seasons.
    8. https://soccer.sportmonks.com/api/v2.0/leagues?include=seasons
    9.  
    10. //Get all Leagues and include both Country and Seasons.
    11. https://soccer.sportmonks.com/api/v2.0/leagues?include=country,seasons
    Now this finally brings us back to match statistics.

    Match statistics themselves are an "included" option when requesting Fixtures - they cannot be requested on their own.
    This means you need to have a Fixture C# class with a child
    ApiData
    that contains the match statisics data.
    For example:
    Code (CSharp):
    1. public class MatchStats
    2. {
    3.   public int team_id;
    4.   public int fixture_id;
    5.   public int fouls;
    6.   //etc...
    7. }
    8.  
    9. public class Fixture
    10. {
    11.   public int id;
    12.   public int league_id;
    13.   public int season_id;
    14.  
    15.   //etc...
    16.  
    17.   public ApiData<MatchStats[]> stats;
    18. }
    Now according to the documentation, it seems like you can only request the match statistics of individual Fixtures, meaning you need to know the ID of the fixture whose stats you want to "include", which is where this example URL shown earlier comes from:
    https://soccer.sportmonks.com/api/v2.0/fixtures/16475287?api_token={API_TOKEN}&include=stats

    This URL is requesting one Fixture with an ID of 16475287 and "including" its match statistics in the response.

    If you want to get many match statisics for many Fixtures, the only real way to do so is to first send an API request to get all Fixtures, then for each fixture, send individual API requests to get their match statistics.

    Doing this isn't really a good idea though, as you'd be spamming API requests which will slow down your application, as well as potentially get you blocked from requesting their API if they determine that you're abusing requests.
    Instead, you should only request all the Fixtures you need first, and then only request the match statistics for a specific Fixture whenever you actually need to view that Fixture's statistics in your application.

    In the documentation for Fixtures, there isn't a way to get all of them in the same way as getting all Leagues, for instance. You need to either specify a date, a date range, or a list of Fixture IDs to get multiple Fixtures.
    They have examples of this here:
    https://football-postman.sportmonks.com/#b7f79045-eac3-40c5-b114-24c5b438f62c

    So if you need to get multiple Fixtures in your application, you may just need to pick a date range that's "good enough" for your needs.

    Putting all of that together, you'd get something like this:
    Code (CSharp):
    1. public class Example
    2. {
    3.   IEnumerator GetRequest<T>(string url, Action<T> callback)
    4.   {
    5.     using(UnityWebRequest request = UnityWebRequest.Get(url))
    6.     {
    7.       yield return request.SendWebRequest();
    8.  
    9.       T response = JsonUtility.FromJson<T>(request.downloadHandler.text);
    10.       callback(response);
    11.     }
    12.   }
    13.  
    14.   void SendRequests()
    15.   {
    16.     //Get Leagues
    17.     string leaguesUrl = "https://soccer.sportmonks.com/api/v2.0/leagues?api_token=API_TOKEN";
    18.     StartCoroutine(GetRequest<ApiData<League[]>>(leaguesUrl, (ApiData<League[]> response) => {
    19.       //etc...
    20.     }));
    21.  
    22.     //Get Fixtures
    23.     string fixturesUrl = "https://soccer.sportmonks.com/api/v2.0/fixtures?api_token=API_TOKEN";
    24.     StartCoroutine(GetRequest<ApiData<Fixture[]>>(leaguesUrl, (ApiData<Fixture[]> response) => {
    25.       GetFixtureStats(response.data[0].id);
    26.     }));
    27.   }
    28.  
    29.   void GetFixtureStats(int fixtureId)
    30.   {
    31.     string url = $"https://soccer.sportmonks.com/api/v2.0/fixtures/{fixtureId}?api_token=API_TOKEN&include=stats";
    32.     StartCoroutine(GetRequest<ApiData<Fixture>>(leaguesUrl, (ApiData<Fixture> response) => {
    33.       MatchStats[] stats = response.data.stats.data;
    34.     }));
    35.   }
    36. }
     
    Last edited: Oct 3, 2021
    Mashimaro7 likes this.
  22. Mashimaro7

    Mashimaro7

    Joined:
    Apr 10, 2020
    Posts:
    727
    Okay, so I got it populating the match stats and fixtures, it's almost done. But what if I want to get the team statistics and everything else? Do I need to call fixtures again, but this time do the team statistics? Ahh, this is all very confusing haha. They sure don't make these API calls very easy lol. It'd be nice if I could put them all in the fixtures class, but each one requires a different URL.

    Sorry if I'm not fully understanding it, I'm a little bit confused on some of the details.

    Also, what is the Acton<T> callback you have there?

    Thanks again for all the help.
     
  23. Vryken

    Vryken

    Joined:
    Jan 23, 2018
    Posts:
    2,106
    Yeah, you would need to create different classes for every different "statistics", since each "statistics" API endpoint returns different types of statistics for different types of data.
    https://docs.sportmonks.com/football/tutorials/statistics

    You'd need a unique parent class for each main data type, and a unique statistics class that each main type contains as a child.
    You can tell which parent/child classes belong to each other from the URLs themselves.

    Using the examples in the docs:
    • /api/v2.0/fixtures/16475287?api_token={API_TOKEN}&include=stats (get fixtures with match stats)
    • /api/v2.0/teams/3468?api_token={API_TOKEN}&include=stats (get teams with team stats)
    • /api/v2.0/players/31000?api_token={API_TOKEN}&include=stats (get players with player stats)
    • /api/v2.0/seasons/16264?api_token={API_TOKEN}&include=stats (get seasons with season stats)
    Based on this:
    • You need a Fixture class with a child ApiData<MatchStats> field (which we already have).
    • You need a Team class with a child ApiData<TeamStats> field.
    • You need a Player class with a child ApiData<PlayerStats> field.
    • You need a Season class with a child ApiData<SeasonStats> field.
    Code (CSharp):
    1. public class TeamStats
    2. {
    3.   //Fields...
    4. }
    5.  
    6. public class Team
    7. {
    8.   //Fields...
    9.  
    10.   public ApiData<TeamStats> stats;
    11. }
    12.  
    13. public class PlayerStats
    14. {
    15.   //Fields...
    16. }
    17.  
    18. public class Player
    19. {
    20.   //Fields...
    21.  
    22.   public ApiData<PlayerStats> stats;
    23. }
    24.  
    25. public class SeasonStats
    26. {
    27.   //Fields...
    28. }
    29.  
    30. public class Season
    31. {
    32.   //Fields...
    33.  
    34.   public ApiData<SeasonStats> stats;
    35. }
    36.  
    An Action in C# is basically a reference to a method:
    Code (CSharp):
    1. public class Example
    2. {
    3.   void Main()
    4.   {
    5.     //myAction is a reference to the LogConsole method.
    6.     Action myAction = LogConsole;
    7.  
    8.     //Calling Invoke will call the LogConsole method.
    9.     myAction.Invoke();
    10.  
    11.     //This is a shorthand for calling Invoke:
    12.     myAction();
    13.   }
    14.  
    15.   void LogConsole()
    16.   {
    17.     Debug.Log("Console message");
    18.   }
    19. }
    Actions can be defined with parameters as well via generics:
    Code (CSharp):
    1. public class Example
    2. {
    3.   void Main()
    4.   {
    5.     //The generic parameters must match the parameters of the method, otherwise a compiler error will occur.
    6.     //LogConsole takes in one string parameter, so myAction is defined with one string generic type.
    7.     Action<string> myAction = LogConsole;
    8.  
    9.     //Invoke the LogConsole method and pass the "Console message" string parameter to it.
    10.     myAction("Console message");
    11.   }
    12.  
    13.   void LogConsole(string message)
    14.   {
    15.     Debug.Log(message);
    16.   }
    17. }
    Actions can also reference lambda expression methods:
    Code (CSharp):
    1. public class Example
    2. {
    3.   void Main()
    4.   {
    5.     Action myAction1 = () => Debug.Log("Invoked myAction1");
    6.  
    7.     Action<string> myAction2 = (string value) => Debug.Log(value);
    8.  
    9.     Action<int, string> myAction3 = (int value1, string value2) =>
    10.     {
    11.       string message = "Values: " + value1 + ", " + value2;
    12.       Debug.Log(message);
    13.     };
    14.  
    15.     myAction1();
    16.     myAction2("Some string value");
    17.     myAction3(55, "Another string value");
    18.   }
    19. }
    So when a coroutine is started with this method...
    Code (CSharp):
    1. IEnumerator GetRequest<T>(string url, Action<T> callback)
    2. {
    3.   using(UnityWebRequest request = UnityWebRequest.Get(url))
    4.   {
    5.     yield return request.SendWebRequest();
    6.  
    7.     T response = JsonUtility.FromJson<T>(request.downloadHandler.text);
    8.     callback(response);
    9.   }
    10. }
    ...It will execute its web request process in the background, and once it's finished, it will invoke the
    callback
    Action with the response data from the request.
    The callback can again either be a method or a lambda expression:
    Code (CSharp):
    1. public class Example : MonoBehaviour
    2. {
    3.   void Main()
    4.   {
    5.     //Here, we pretend to send a request that returns a List of strings.
    6.     //When the request finishes, OnRequest1Complete will be called with the response data passed to it.
    7.     StartCoroutine(GetRequest<List<string>>("path/to/url", OnRequest1Complete));
    8.  
    9.     //Here, we pretend to send a request that returns a List of strings.
    10.     //When the request finishes, the lambda expression method will be called with the response data passed to it.
    11.     StartCoroutine(GetRequest<List<string>>("path/to/url", (List<string> data) =>
    12.     {
    13.       //Do something with response data...
    14.     }));
    15.   }
    16.  
    17.   void OnRequest1Complete(List<string> data)
    18.   {
    19.     //Do something with response data...
    20.   }
    21.  
    22.   IEnumerator GetRequest<T>(string url, Action<T> callback) { //etc... }
    23. }
    You can read more about Actions here:
    https://docs.microsoft.com/en-us/dotnet/api/system.action?view=net-5.0
    https://www.tutorialsteacher.com/csharp/csharp-action-delegate
     
    Last edited: Dec 29, 2021
    Mashimaro7 likes this.