Search code examples
c#jsonavalonia

Avalonia, Foreach of an API request with HttpClient


I currently have an API request that I make with Avalonia C# and I manage to return the entire JSON as a result but cannot retrieve each element in a foreach. Here is the current code:

// Get servers infos
private async Task GetServersInfos()
{

    using (var httpClient = new HttpClient())
    {
        using (var request = new HttpRequestMessage(new HttpMethod("GET"), "http://example.com/api"))
        {
            var response = await httpClient.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                string jsonObject = JsonSerializer.Serialize(response.Content);
                var results = await response.Content.ReadAsStringAsync();

                foreach (var result in results)
                {
                    TestText.Text = result["name"];
                }

            }
        }
    }
}

And the JSON result:

{
    "1": {
        "name": "Example 1",
        "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit...",
    },
    "2": {
        "name": "Example 2",
        "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit...",
    }
}

But I have the following error: Unable to apply indexing using [] to an expression of type 'char'.


Solution

  • You must use JsonSerializer.DeserializeAsync to convert the known JSON to a C# data type or use JsonDocument to convert the unknown JSON response to a read-only DOM model and traverse its element nodes.

    To improve the performance, you should pass a Stream to JsonSerializer.SeserializeAsync as this eliminates one complete enumeration of the response message. To issue a HTTP GET request, simply use one of the Get... methods provided by the HttpClient API.
    In our case we pick the HttpClient.GetStreamAsync method.

    The response message is a single JSON object (and not an array) that is constructed of key-value pairs. That makes a list of key-value pairs. Which is exactly how the Dictionary structure is built. That's a crucial finding as this means we can deserialize every well-formed JSON object into a Dictionary<string, object>.
    The only special case is if the JSON contains an array. In this case we must tell the JsonSerializer how to convert it, which means we have to create a custom JsonConverter. In any other case we can always use Dictionary<string, string> to deserialize or serialize a JSON object.
    However, to handle complex unknown JSON content the recommended way would be to use JsonDocument.

    Example 1

    Deserialize a known simple JSON that contains no arrays in a very general manner using JsonSerializer (doesn't require a custom data structure).

    In your example case we expect a list of nested JSON objects, which can be represented a nested Dictionary in the form of Dictionary<string, Dictionary<string, string>>:

    // Get server JSON response as Stream
    private async Task GetServersInfos()
    {
      using var httpClient = new HttpClient()
      string url = "http://example.com/api";
      await using Stream responseStream = await httpClient.GetStreamAsync(url);;
    
      var serializerOptions = new JsonSerializerOptions() { PropertyNameCaseInsensitive = true };
      Dictionary<string, Dictionary<string, string>>? responseData = await JsonSerializer.DeserializeAsync<Dictionary<string, Dictionary<string, string>>>(responseStream, serializerOptions);
    
      /* * * * * * * * * * * * * * * * * * * * * * * * *
       *                                               *
       *  Example how to handle the response object    *
       *                                               *
       * * * * * * * * * * * * * * * * * * * * * * * * */
    
      Dictionary<string, string> firstDataEntry = responseData["1"];
      string nameValue = firstDataEntry["name"]; // "Example 1"
      string descriptionValue = firstDataEntry["description"]; // "Lorem ipsum dolor sit amet, consectetur adipiscing elit..."
    
      Dictionary<string, string> secondDataEntry = responseData["2"];
      nameValue = secondDataEntry["name"]; // "Example 2"
      descriptionValue = secondDataEntry["Description"]; // "Lorem ipsum dolor sit amet, consectetur adipiscing elit..."
    
      // Or enumerate the response JSON:
      foreach (KeyValuePair<string, Dictionary<string, string>> entry in responseData)
      {
        string jsonObjectPropertyName = entry.Key; // "1"
        Dictionary<string, string> nestedJsonObject = entry.Value;
        foreach (KeyValuePair<string, string> nestedObjectEntry in nestedJsonObject)
        {
          jsonObjectPropertyName = nestedObjectEntry.Key; // "name" or "description"
          string jsonObjectPropertyValue = nestedObjectEntry.Value; // "Example 1" or "Lorem ipsum dolor sit amet, consectetur adipiscing elit..."
    
          // For example, only get the "description" value
          if (nestedObjectEntry.Key.Equals("Description", StringComparison.OrdinalIgnoreCase))
          {
            string descriptionText = nestedObjectEntry.Value; // "Lorem ipsum dolor sit amet, consectetur adipiscing elit..."
          }
        }
      }
    }
    

    Example 2

    Deserialize a known JSON that contains no arrays in a very specialized manner using JsonSerializer.

    In your example case we expect a list of nested JSON objects, which enables us to create a C# type for improved convenience in terms of data handling:

    DataSet.cs ``c# public class DataSet { public string Name { get; } public string Description { get; } }

    ```c#
    // Get server JSON response as Stream
    private async Task GetServersInfos()
    {
      using var httpClient = new HttpClient()
      string url = "http://example.com/api";
      await using Stream responseStream = await httpClient.GetStreamAsync(url);
    
      var serializerOptions = new JsonSerializerOptions() { PropertyNameCaseInsensitive = true };
      Dictionary<int, DataSet>? responseData = await JsonSerializer.DeserializeAsync<Dictionary<int, DataSet>>(responseStream, serializerOptions);
    
      /* * * * * * * * * * * * * * * * * * * * * * * * *
       *                                               *
       *  Example how to handle the response object    *
       *                                               *
       * * * * * * * * * * * * * * * * * * * * * * * * */
    
      DataSet firstDataEntry = responseData[1];
      string nameValue = firstDataEntry.Name; // "Example 1"
      string descriptionValue = firstDataEntry.Description; // "Lorem ipsum dolor sit amet, consectetur adipiscing elit..."
    
      DataSet secondDataEntry = responseData[2];
      nameValue = secondDataEntry.Name; // "Example 2"
      descriptionValue = secondDataEntry.Description; // "Lorem ipsum dolor sit amet, consectetur adipiscing elit..."
    }
    

    Example 3

    Deserialize an unknown JSON using JsonDocument.

    We can traverse and inspect every JSON using the JSON's read-only DOM. If we need to manipulate the JSON document we would use JsonNode instead.
    However, JsonDocument seems to be sufficient and it is faster than JsonNode:

    // Get server JSON response as Stream
    private async Task GetServersInfos()
    {
      using var httpClient = new HttpClient()
      string url = "http://example.com/api";
      await using Stream responseStream = await httpClient.GetStreamAsync(url);
      using JsonDocument responseJson = await JsonDocument.ParseAsync(responseStream);
    
      /* * * * * * * * * * * * * * * * * * * * * * * * *
       *                                               *
       *  Example how to handle the response object    *
       *                                               *
       * * * * * * * * * * * * * * * * * * * * * * * * */
    
      // Because the response is an JSON object and not a JSON array
      // we have to call EnumerateObject instead of EnumerateArray
      if (responseJson.RootElement.ValueKind is JsonValueKind.Object)
      {
        foreach (JsonProperty jsonObject in responseJson.RootElement.EnumerateObject())
        {
          string propertyName = jsonObject.Name; // "1" and "2"
          JsonElement nestedObject = jsonObject.Value;
      
          // Enumerate the JSON object, which has two properties "name" and "description",
          // property by property (makes for two iterations)
          if (nestedObject.ValueKind is JsonValueKind.Object)
          {
            foreach (JsonProperty dataEntry in nestedObject.EnumerateObject())
            {
              string entryName = dataEntry.Name; // "name" and "description"
              string? entryValue = dataEntry.Value.GetString(); // "Example 1" and "Lorem ipsum dolor sit amet, consectetur adipiscing elit..."
    
              // For example, only get the "description" propertyValue
              if (dataEntry.Name.Equals("Description", StringComparison.OrdinalIgnoreCase))
              {
                string descriptionText = dataEntry.Value.GetString();
              }
            }
          }
        }
      }
    }