Nexta Tech Blog
🧑‍🎓

クラス名が違ってもAPI連携は動くけど「型」と「大文字小文字」には気をつけよう

に公開

背景:WinForms から Blazor への移行と「型」の再定義

現在、社内で稼働しているレガシーな Windows Forms アプリケーションを、モダンな Blazor アプリケーションへ移行するプロジェクトを進めています。

この移行において、サーバーサイドの既存 API はそのまま流用し、フロントエンドのみを刷新するという構成を取る場合が多々ありますが、実装中にふと手が止まりました。 以下のような、API呼び出しのコードを書こうとしたときのことです。

// 【Blazor側の実装イメージ】
// サーバーは "M_UnitPrices" (マスタ) のリストを返してくるが、
// 画面側では "L_UnitPrice" (DTO) として受け取りたい。

public async Task<List<L_UnitPrice>> GetAllAsync()
{
    // クラス名が不一致でも、GetFromJsonAsync は通るのか…?
    return await _httpClient.GetFromJsonAsync<List<L_UnitPrice>>("api/unitprices");
}

サーバーが返すのは、レガシーな命名規則の M_UnitPrices です。 対して、私が受け取ろうとしているのは、新しく定義した L_UnitPrice です。

「これ、クラス名が違うのにエラーにならずにデータが入るのか?」

HTTP通信で返ってくるのは所詮 JSON テキストなので[1]、直感的には「形(プロパティ)さえ合えば入る」はずです。しかし、実際の移行作業で「たぶん動く」で進めるのはリスクがあります。

そこで、この挙動を確実なものにするため、通信部分と変換部分を切り分け、System.Text.Json の変換ロジックのみを抽出して検証を行いました。

実験1:クラス名違い(成功パターン)

まずは基準となる「成功パターン」です。
クラス名が全く違っても、プロパティ名と型が一致していればデータはマッピングされます。

Program.cs
using System.Text.Json;

namespace MyJsonApp
{
    public class Program
    {
        static void Main(string[] args)
        {
            var mProduct = new M_Product(101, "Test Item");
            var json = JsonSerializer.Serialize(mProduct);

            // JSON: {"Id":101,"Name":"Test Item"}
            Console.WriteLine($"JSON: {json}");

            var result = JsonSerializer.Deserialize<L_Product>(json);
            Console.WriteLine($"成功: Id={result.Id}, Name={result.Name}");
            // 出力 -> 成功: Id=101, Name=Test Item

            Console.ReadLine();
        }
    }

    // --- 以下、クラス定義 ---

    // サーバー側想定
    public class M_Product
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public M_Product() { }

        public M_Product(int id, string name)
        {
            Id = id;
            Name = name;
        }
    }

    // クライアント側
    public class L_Product
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public L_Product() { }

        public L_Product(int id, string name)
        {
            Id = id;
            Name = name;
        }
    }
}

結果

JSON: {"Id":101,"Name":"Test Item"}
成功: Id=101, Name=Test Item

ここまでは予想通りです。JSONには「クラス名」の情報が含まれないため、形さえ合えば入ります。

実験2:型不一致(失敗パターン)

次に、プロパティ名は合っているが、「データ型」が違う場合です。
例えば、JSON側は数値(Number)なのに、受け取るC#側が文字列(String)として定義されていたらどうなるでしょうか?

Program.cs
using System.Text.Json;

namespace MyJsonApp
{
    public class Program
    {
        static void Main(string[] args)
        {
            try
            {
                // サーバー側のデータを模倣(数値の1000)
                var mPrice = new M_Price(1000);
                var json = JsonSerializer.Serialize(mPrice);

                // JSON: {"Value":1000}  <- 数値型として出力される
                Console.WriteLine($"JSONデータ: {json}");

                Console.WriteLine("デシリアライズ(復元)を試みます...");

                var result = JsonSerializer.Deserialize<L_Price>(json);

                Console.WriteLine($"成功: Value={result.Value}");
            }
            catch (JsonException ex)
            {
                Console.WriteLine("--------------------------------------------------");
                Console.WriteLine($"エラー発生: {ex.Message}");
                Console.WriteLine("--------------------------------------------------");
            }

            Console.ReadLine();
        }
    }

    // --- クラス定義 ---

    // サーバー側:価格を数値(int)で持つクラス
    public class M_Price
    {
        public int Value { get; set; }

        public M_Price() { }
        public M_Price(int value)
        {
            Value = value;
        }
    }

    // クライアント側:価格を文字列(string)で受け取ろうとするクラス
    public class L_Price
    {
        public string Value { get; set; }

        public L_Price() { }
        public L_Price(string value)
        {
            Value = value;
        }
    }
}

結果

JSONデータ: {"Value":1000}
デシリアライズ(復元)を試みます...
--------------------------------------------------
エラー発生: The JSON value could not be converted to System.String. Path: $.Value | LineNumber: 0 | BytePositionInLine: 13.
--------------------------------------------------

例外(JsonException)が発生しました。
System.Text.Json はデフォルトでは厳格で、JSONの数値型を勝手に文字列型に変換してくれたりはしません。型の一致は必須条件です。

実験3:大文字・小文字不一致(スルーされるパターン)

最後に、型は合っているが、「プロパティ名の大文字・小文字」が違う場合です。
Web API(JSON)の世界ではキャメルケース(firstName)、C#の世界ではパスカルケース(FirstName)が標準です。この違いはどう扱われるでしょうか?

Program.cs
using System.Text.Json;

namespace MyJsonApp
{
    public class Program
    {
        static void Main(string[] args)
        {
            var mCamel = new M_Camel(999);
            var json = JsonSerializer.Serialize(mCamel);
            // JSON: {"id":999}

            var result = JsonSerializer.Deserialize<L_Pascal>(json);

            // デフォルトでは大文字小文字が区別されるため、一致せず 0 になる
            Console.WriteLine($"結果: Id={result.Id}");

            Console.ReadLine();
        }
    }

    // サーバー側
    public class M_Camel
    {
        public int id { get; set; } // プロパティ名を小文字に設定

        public M_Camel() { }
        public M_Camel(int id)
        {
            this.id = id;
        }
    }

    // クライアント側
    public class L_Pascal
    {
        public int Id { get; set; } // プロパティ名を大文字に設定

        public L_Pascal() { }
        public L_Pascal(int id)
        {
            Id = id;
        }
    }
}

結果

結果: Id=0

エラーにはなりませんが、値が入りません(デフォルト値の0)。

これは「ハマりどころ」です。System.Text.Json はデフォルトで大文字・小文字を区別します。
そのため、idId は「別のプロパティ」とみなされ、マッピング対象が見つからず無視されたのです。

※これを回避するには、JsonSerializerOptionsPropertyNameCaseInsensitive = true を設定する必要があります。

var option = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
var result = JsonSerializer.Deserialize<L_Pascal>(json, option);

まとめ

API連携における「型合わせ」の境界線が見えてきました。

  1. クラス名: 違ってもOK(JSONに含まれないから)。
  2. データ型: 一致必須(違うと JsonException)。
  3. 大文字・小文字: 一致必須(違うと無視される)。

開発中のアプリで M_L_ が繋がっていたのは、クラス名は違えど、プロパティ定義(名前と型)が完璧に一致していたからこそ成り立っていたバランスだったわけです。

脚注
  1. HTTP の決まり RFC 9110
    Webサーバーとクライアントは、「ヘッダー(メタ情報)」と「ボディ(中身)」で会話するというルールです。ここで重要なのが Content-Type ヘッダーです。サーバーは RFC 9110 (HTTP) の仕組みに従って Content-Type ヘッダーを付与します。その際、中身がJSONであることを伝えるために、RFC 8259 (JSON) で定義された application/json という値を指定します。ASP.NET はこの標準仕様に準拠して実装されているため、自動的にこのヘッダーが付与されます。 ↩︎

Nexta Tech Blog
Nexta Tech Blog

Discussion