Utf8Json

シリアライゼーションC#.NETJSON高性能ゼロアロケーション

ライブラリ

Utf8Json - C#向け最速・ゼロアロケーションJSONシリアライザー

概要

Utf8Jsonは、C#(.NET、.NET Core、Unity、Xamarin)向けに開発された超高速JSONシリアライザーです。UTF-8バイナリを直接読み書きすることで、文字列変換のオーバーヘッドを完全に排除し、ゼロアロケーションを実現しています。動的なILコード生成、オートマタベースのプロパティマッチング、独自の数値変換アルゴリズムなど、徹底的なパフォーマンス最適化により、既存のJSONライブラリを大幅に上回る性能を達成しています。

詳細

Utf8Jsonは、MessagePackSharpやZeroFormatterの作者であるneuecc(Yoshifumi Kawai)氏が開発した、パフォーマンスに特化したJSONシリアライザーです。従来のJSONライブラリがUTF-16文字列への変換を必要とするのに対し、Utf8JsonはUTF-8バイト配列を直接処理することで、劇的なパフォーマンス向上を実現しています。

ライブラリの核心的な特徴は、ゼロアロケーション設計です。64KB以下のバッファはスレッドローカルメモリプールから取得され、ガベージコレクションの負荷を最小限に抑えます。また、実行時に動的にILコードを生成してカスタム型用の最適化されたフォーマッターを作成し、AOT環境向けには事前コード生成もサポートしています。

プロパティマッチングにはオートマタベースのアプローチを採用し、生のUTF-8バイトから直接プロパティ名を照合します。数値変換では独自のitoa/atoi、dtoa/atodアルゴリズムを実装し、文字列を経由しない直接変換を実現。これらの最適化により、既存のJSONライブラリと比較して2〜4倍の性能向上を達成しています。

注意点として、オリジナルのリポジトリはアーカイブされており、現在はコミュニティフォークの使用が推奨されています。

メリット・デメリット

メリット

  • 圧倒的な高速性: 既存のJSONライブラリの2〜4倍のパフォーマンス
  • ゼロアロケーション: メモリプール使用でGC負荷を最小化
  • 直接UTF-8処理: 文字列変換のオーバーヘッドを完全に排除
  • Unity対応: Vector3、Quaternionなど Unity固有型をネイティブサポート
  • 柔軟なカスタマイズ: Resolver/Formatterシステムで高度な制御が可能
  • AOT環境対応: 事前コード生成によりXamarin、Unity IL2CPPで動作

デメリット

  • メンテナンス停止: オリジナルリポジトリはアーカイブ状態
  • 複雑性: 高度な最適化により内部実装が複雑
  • 機能制限: パフォーマンス優先のため一部高度な機能(循環参照等)未対応
  • 学習曲線: Resolver/Formatterシステムの理解に時間が必要
  • デバッグ困難: 動的生成されたILコードのデバッグが困難
  • 標準化の欠如: System.Text.Jsonの登場により標準ライブラリとの差が生じる

参考ページ

書き方の例

1. 基本的なシリアライゼーション

using Utf8Json;

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public DateTime BirthDate { get; set; }
}

class BasicExample
{
    static void Main()
    {
        var person = new Person 
        { 
            Name = "田中太郎",
            Age = 30,
            BirthDate = new DateTime(1993, 5, 15)
        };

        // オブジェクト → byte[](UTF-8)
        byte[] utf8Json = JsonSerializer.Serialize(person);
        
        // byte[] → オブジェクト
        var deserialized = JsonSerializer.Deserialize<Person>(utf8Json);
        
        // オブジェクト → 文字列
        string jsonString = JsonSerializer.ToJsonString(person);
        Console.WriteLine(jsonString);
        
        // ストリームへの書き込み
        using (var stream = new FileStream("person.json", FileMode.Create))
        {
            JsonSerializer.Serialize(stream, person);
        }
    }
}

2. プライベートメンバーのシリアライゼーション

public class Employee
{
    private string employeeId = "EMP-001";
    private double salary = 50000;
    
    public string Name { get; set; }
    public string Department { get; set; }
    
    // プライベートフィールドへのアクセサー
    public string GetEmployeeId() => employeeId;
    public double GetSalary() => salary;
}

class PrivateMemberExample
{
    static void SerializePrivateMembers()
    {
        var employee = new Employee 
        { 
            Name = "山田花子",
            Department = "営業部"
        };
        
        // プライベートメンバーを含めてシリアライズ
        var json = JsonSerializer.ToJsonString(employee, StandardResolver.AllowPrivate);
        Console.WriteLine(json);
        // 出力: {"employeeId":"EMP-001","salary":50000,"Name":"山田花子","Department":"営業部"}
        
        // プライベートメンバーも復元してデシリアライズ
        var deserialized = JsonSerializer.Deserialize<Employee>(json, StandardResolver.AllowPrivate);
        Console.WriteLine($"ID: {deserialized.GetEmployeeId()}, 給与: {deserialized.GetSalary()}");
    }
}

3. カスタムリゾルバーとフォーマッター

using Utf8Json.Formatters;
using Utf8Json.Resolvers;

// カスタムフォーマッター
public class DateOnlyFormatter : IJsonFormatter<DateTime>
{
    public void Serialize(ref JsonWriter writer, DateTime value, IJsonFormatterResolver formatterResolver)
    {
        writer.WriteString(value.ToString("yyyy-MM-dd"));
    }
    
    public DateTime Deserialize(ref JsonReader reader, IJsonFormatterResolver formatterResolver)
    {
        var dateString = reader.ReadString();
        return DateTime.ParseExact(dateString, "yyyy-MM-dd", null);
    }
}

// カスタムリゾルバー
public class CustomResolver : IJsonFormatterResolver
{
    public static IJsonFormatterResolver Instance = new CustomResolver();
    
    private CustomResolver() { }
    
    public IJsonFormatter<T> GetFormatter<T>()
    {
        if (typeof(T) == typeof(DateTime))
        {
            return (IJsonFormatter<T>)new DateOnlyFormatter();
        }
        
        return StandardResolver.Default.GetFormatter<T>();
    }
}

// 使用例
class CustomFormatterExample
{
    static void UseCustomFormatter()
    {
        var data = new { Date = DateTime.Now, Name = "テスト" };
        
        var json = JsonSerializer.ToJsonString(data, CustomResolver.Instance);
        Console.WriteLine(json); // {"Date":"2023-12-15","Name":"テスト"}
    }
}

4. Unity向けの使用例

using UnityEngine;
using Utf8Json;
using Utf8Json.Resolvers;

[System.Serializable]
public class GameData
{
    public string PlayerName { get; set; }
    public int Score { get; set; }
    public Vector3 Position { get; set; }
    public Quaternion Rotation { get; set; }
    public Color PlayerColor { get; set; }
}

public class UnityExample : MonoBehaviour
{
    void Start()
    {
        // Unityリゾルバーをデフォルトに設定
        JsonSerializer.SetDefaultResolver(StandardResolver.Default);
        
        var gameData = new GameData
        {
            PlayerName = "Player1",
            Score = 1000,
            Position = new Vector3(10f, 5f, 3f),
            Rotation = Quaternion.Euler(0, 45, 0),
            PlayerColor = Color.red
        };
        
        // Unity型を含むオブジェクトのシリアライズ
        byte[] jsonBytes = JsonSerializer.Serialize(gameData);
        
        // PlayerPrefsに保存
        string base64 = System.Convert.ToBase64String(jsonBytes);
        PlayerPrefs.SetString("GameData", base64);
        
        // 読み込み
        string loadedBase64 = PlayerPrefs.GetString("GameData");
        byte[] loadedBytes = System.Convert.FromBase64String(loadedBase64);
        var loadedData = JsonSerializer.Deserialize<GameData>(loadedBytes);
        
        Debug.Log($"Loaded: {loadedData.PlayerName}, Score: {loadedData.Score}");
    }
}

5. 動的JSON処理

class DynamicJsonExample
{
    static void ProcessDynamicJson()
    {
        // 動的JSONのデシリアライズ
        var json = @"{
            ""name"": ""商品A"",
            ""price"": 1500,
            ""tags"": [""新商品"", ""おすすめ""],
            ""details"": {
                ""weight"": 500,
                ""dimensions"": {
                    ""width"": 10,
                    ""height"": 20,
                    ""depth"": 5
                }
            }
        }";
        
        dynamic obj = JsonSerializer.Deserialize<dynamic>(json);
        
        // 動的アクセス
        string name = obj["name"];                    // "商品A"
        double price = obj["price"];                  // 1500
        var firstTag = obj["tags"][0];               // "新商品"
        var weight = obj["details"]["weight"];       // 500
        var width = obj["details"]["dimensions"]["width"]; // 10
        
        Console.WriteLine($"商品: {name}, 価格: {price}円");
        
        // 動的オブジェクトの構築
        var newObj = new Dictionary<string, object>
        {
            ["id"] = 123,
            ["items"] = new[] { "item1", "item2" },
            ["metadata"] = new Dictionary<string, object>
            {
                ["created"] = DateTime.Now,
                ["version"] = 1.0
            }
        };
        
        string newJson = JsonSerializer.ToJsonString(newObj);
        Console.WriteLine(newJson);
    }
}

6. 高度なパフォーマンス最適化

using Utf8Json;
using System.Buffers;

class PerformanceOptimization
{
    // ArraySegmentを使用したゼロコピー処理
    static void ZeroCopyProcessing()
    {
        var largeObject = GenerateLargeObject();
        
        // ArraySegmentで内部バッファプールを使用
        ArraySegment<byte> segment = JsonSerializer.SerializeUnsafe(largeObject);
        
        try
        {
            // セグメントを直接処理(コピーなし)
            ProcessJsonBytes(segment.Array, segment.Offset, segment.Count);
        }
        finally
        {
            // 重要: ArraySegmentは使用後すぐに返却
            JsonSerializer.ReturnArrayToPool(segment.Array);
        }
    }
    
    // ストリーミング処理
    static async Task StreamingSerializationAsync()
    {
        var items = GetLargeDataset(); // 大量のデータ
        
        using (var stream = new FileStream("large.json", FileMode.Create))
        using (var writer = new StreamWriter(stream))
        {
            await writer.WriteAsync("[");
            
            bool first = true;
            foreach (var item in items)
            {
                if (!first) await writer.WriteAsync(",");
                first = false;
                
                // 各アイテムを個別にシリアライズ
                var json = JsonSerializer.ToJsonString(item);
                await writer.WriteAsync(json);
            }
            
            await writer.WriteAsync("]");
        }
    }
    
    // 事前生成されたフォーマッターの使用(AOT環境向け)
    static void UsePreGeneratedFormatter()
    {
        // AOT環境では事前生成が必要
        // Utf8Json.UniversalCodeGenerator.exe を使用して生成
        
        Utf8Json.Resolvers.GeneratedResolver.Instance.Register();
        
        var obj = new MyGeneratedType { Value = 42 };
        byte[] json = JsonSerializer.Serialize(obj);
    }
}