petitviolet blog

    Working with Json over HTTP in Unity

    2022-09-15

    UnityC#

    Unity provides handy class called UnityWebRequest for sending HTTP requests.
    See the document for details. Unity - Scripting API: UnityWebRequest

    As the document illustrates, UnityWebRequest handles Web requests with depending on DownloadHandler and UploadHandler.

    Serialize and deserialize C# objects to Json

    Using JsonUtility would be the best way of working with Json in Unity. Many alternative libraries like Json.NET there are, but JsonUtility lives in Unity natively. In addition, it seems JsonUtility shows the fastest performance in de-/serialization according to JSON Libraries Comparison in Unity 5.5, though it's a bit old.

    So, this section is to describe how to use JsonUtility in a nutshell.

    This json represents a save data in an example game.

    {
      "name": "alice",
      "highScore": 12345,
      "items": [
        {
          "id": 1,
          "name": "sword"
        },
        {
          "id": 2,
          "name": "shield"
        }
      ]
    }
    

    To de-/serialize this structure, the following code snippet should work.

    using System;
    using UnityEngine;
    
    [Serializable]
    public class SaveGame
    {
        [SerializeField] private string name;
        [SerializeField] private int highScore;
        [SerializeField] private List<Item> items;
    
        public string Name => name;
        public int HighScore => highScore;
        public List<Item> Items => items;
    }
     
    [Serializable]
    public class Item
    {
    	[SerializeField] private int id;
    	[SerializeField] private string name;
    
    	public int Id => id;
    	public string Name => name;
    }
    

    Serializable annotation to make a class to be de-/serializable, SerializeField annotation to let a field visible to de-/serializer even when it's a private field. Based on C# Coding Conventions published by Microsoft, public field names should be PascalCase, not camelCase. And in json, using camelCase or snake_case for field names would be standard I believe. So, using camelCase for field names to de-/serialize json fields and exposing them by expression-bodied member to follow the coding conventions.

    In addition, there are other limitations in de-/serializing C# objects with Json. See Unity - Manual: Script serialization for more details.

    Json over HTTP

    Next step is to send/receive Json payloads over HTTP in Unity.

    Following code snippes shows a simple implementation for sending a HTTP requests with serializing objects into Json, and then receiving a response with deserializing body into C# objects.

    jsonhttp.cs
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using Cysharp.Threading.Tasks;
    using UnityEngine;
    using UnityEngine.Networking;
    
    namespace MyNamespace
    {
        public class JsonHttp
        {
            // HTTP specific Exception class
            public class HttpError : Exception
            {
                public readonly long StatusCode;
    
                public HttpError(long statusCode, string message) : base(message)
                {
                    StatusCode = statusCode;
                }
                public HttpError(long statusCode, string message, Exception baseException) : base(message, baseException)
                {
                    StatusCode = statusCode;
                }
                override public string ToString()
                {
                    return $"HttpError(statusCode: {StatusCode}, message: {Message})";
                }
            }
    
            private readonly Uri _baseUri;
    
            public JsonHttp(string baseUri)
            {
                this._baseUri = new Uri(baseUri);
            }
    
            public async Task<T> Get<T>(string path, Dictionary<string, string> query = null, Dictionary<string, string> headers = null, int timeoutSec = 3)
            {
                var urlParameter = query != null ? $"?{String.Join("&", query.Select(pair => pair.Key + "=" + pair.Value))}" : "";
                var url = new Uri(_baseUri, $"{path}{urlParameter}");
                var request = UnityWebRequest.Get(url);
                return await ProcessJsonRequest<T>(request, headers, timeoutSec);
            }
    
            public async Task<T> Post<T>(string path, object requestBody, Dictionary<string, string> headers = null, int timeoutSec = 3)
            {
                var url = new Uri(_baseUri, path);
                // serialize T into Json
                string body = JsonUtility.ToJson(requestBody);
                var request = new UnityWebRequest(url, "POST");
                request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(body));
                request.downloadHandler = new DownloadHandlerBuffer();
                return await ProcessJsonRequest<T>(request, headers, timeoutSec);
            }
    
            private async Task<T> ProcessJsonRequest<T>(UnityWebRequest request, Dictionary<string, string> headers, int timeoutSec)
            {
                request.timeout = timeoutSec;
    
                // Give Json related headers by default
                request.SetRequestHeader("Content-Type", "application/json");
                request.SetRequestHeader("Accept", "application/json");
    
                if (headers != null)
                {
                    foreach (var entry in headers)
                    {
                        request.SetRequestHeader(entry.Key, entry.Value);
                    }
                }
    
                try
                {
                    await request.SendWebRequest().WithCancellation(CancellationToken.None);
    
                    string text = request.downloadHandler.text;
    
                    // Consider the request succeeded only when it gets 200-399 HTTP status code
                    if (request.result == UnityWebRequest.Result.Success && request.responseCode >= 200 && request.responseCode < 400)
                    {
                        // deserialize response body into T
                        return JsonUtility.FromJson<T>(text);
                    }
                    else
                    {
                        throw new HttpError(request.responseCode, request.error.Length > 0 ? request.error : text);
                    }
                }
                catch (Exception e)
                {
                    throw new HttpError(request.responseCode, $"ProcessJsonRequest failed due to {e}");
                }
            }
        }
    }
    

    As the snippet and inline comments tell, using JsonUtility.ToJson to serialize an object into Json representation, and using JsonUtility.FromJson<T> to deserialize a string into T object. In terms of handling HTTP POST request, giving UploadHandlerRaw and DownloadHandlerBuffer to UnityWebRequest instance seems to be needed for being able to sending/receiving payload expectedly as far as I tried.