Skip to content

MS_JSONParseExperiments

nishi_74322014 edited this page Aug 19, 2026 · 1 revision

JSONのparseを色々試してみた。

概要

とある案件の要件で、

非構造化データを粘土細工のように
ゴリゴリと処理したいが、そういったことは、可能なのだろうか?

ということを調査した。

結論としては、コチラの LINQ to JSON を使用することで可能。

補足(本ページの構成): 検証記録のため長いが、
論旨は一本道である。

① JSON に合わせてクラスを定義する(基本)
     ↓
② 型が特定できる構造なら、JSON.NET が全部やってくれる
     ↓
③ 【型が特定できない構造】だと POCO にマップできない
     ↓
④ その場合は JObject(LINQ to JSON)で手動で扱う  ← 結論

つまり、**「静的な型付けができない JSON をどう扱うか」**という
一貫した問いに対する調査記録である。

JSON フォーマットとクラスの定義

JSON フォーマットの確認

まず、どのようなフォーマットの JSON を出力したいのかを確認する。

クラスの定義

次に、出力したい JSON に合わせて、クラスを定義する。

補足: この順序(JSON が先、クラスが後)が重要で、
JSONを送信するRESTサービスを作成する方法
「JSON フォーマットを、コントラクトとする」と同じ主張である。

基本

基本的には、以下のルールに従って、クラス・プロパティを定義する。

  • JSON オブジェクト({ キー名 : 値 })の場合
    • 「キー名」を名前に持つプロパティを定義する
  • JSON 配列([ 値1, 値2, ... ])の場合
    • リストまたは配列のプロパティを定義する
  • キー名が不定の場合
    • キー名が不定(実行時に決まる)の場合、事前のクラス/プロパティ定義ができない。
    • このようなときは、Dictionary<TKey, TValue> を、Dictionary<string, object> として定義する。

ルール

基本的に、以下のように JSON 出力を想定し、クラスを定義する。

# 出力したい JSON 定義するクラス・プロパティ
1 { "key1" : "value1", "key2" : "value2" } public class Sample
{
  string key1 { get; set; }
  string key2 { get; set; }
}

若しくは、
Dictionary<string, string> dic;
2 [ "1", "2", "3" ] List<string> listData { get; set; }
3 { "key1" : [ "1", "2", "3" ] } public class Sample
{
  List<string> key1 { get; set; }
}
4 { "key" : { "key1" : "value1", "key2" : "value2" } } public class Sample
{
  string key1 { get; set; }
  string key2 { get; set; }
}
public class Sample2
{
  Sample key { get; set; }
}

若しくは、
Dictionary<string, Sample> dic;

若しくは、
Dictionary<string, Dictionary<string, string>> dic;
5 [ { "key1" : "value1", "key2" : "value2" },
{ "key1" : "value3", "key2" : "value4" } ]
public class Sample
{
  string key1 { get; set; }
  string key2 { get; set; }
}

List<Sample> list;
  • 補足
    • ただし、Dictionary を DataContractJsonSerializer で
      シリアライズ・デシリアライズする場合、下記の様に、相互運用性に問題があるので注意する。
    • 上記のように、public class Sample を定義するのが面倒な場合は、
      以下のように、匿名型を使用することで、簡単に JSON を作成することができる。
JsonConvert.SerializeObject(new
    {
        date = DateTime.Now,
        command = SqlExecuteType.Reader,
        text = this.ClearText(this._commandText),
        param = this._commandParameters,
        ms = this._stopwatch.ElapsedMilliseconds
    }, Formatting.None));

補足(匿名型が有効な場面): 匿名型は
**「一度きりの、返すだけの JSON」**に非常に有効である。

場面 評価
ログ出力、デバッグ情報 最適(上のコード例がまさにこれ)
単発の API の応答 有効
公開 API の応答 非推奨契約が型として残らない

3 つ目が要点で、匿名型は
「どんな JSON を返す API なのか」がコード上に定義として残らない
JSONを送信するRESTサービスを作成する方法で述べた
スキーマ ファーストの観点では、
公開する API では明示的なクラス(DTO)を定義すべきである。

なお、C# 9 以降は record を使うと
匿名型に近い簡潔さで名前付きの型を定義できる。

Web Essentialsを使用

Visual Studio の拡張機能である、Web Essentials を使うと、JSON からクラスを自動生成できる。
Web Essentials は、Visual Studio のメニューから、[ツール]-[拡張機能と更新プログラム]で追加できる。

Web Essentials

JSON 文字列をコピーし、Visual Studio のエディターで、右クリックまたは[編集]メニューから、
[形式を選択して貼り付け]-[JSON をクラスとして貼り付ける]を選択すると、
その JSON フォーマットに合ったクラスが生成される。

補足(最新化:拡張機能なしで使える): この機能は
Visual Studio 2013 以降、標準機能として組み込まれている
Web Essentials を追加する必要は無い。

[編集]→[形式を選択して貼り付け]
  ├─ JSON をクラスとして貼り付ける
  └─ XML をクラスとして貼り付ける

JSON をクリップボードにコピーした状態でないとメニューが出ない
点に注意する。

なお、生成されるクラスは

  • プロパティ名が RootobjectClass1 といった機械的な名前になる、
  • null の項目は objectになる、
  • 数値が int になる(実際には longdecimal が必要かもしれない)

ため、生成後に手で調整する前提で使うのが正しい。
「たたき台を作る」道具である。

JSON.NET

ココが参考になる。

基本(Bean, POCO)

シリアライズ・デシリアライズ

[JsonObject("aaa")]
public class AAA
{
  [JsonProperty("prop1")]
  public int Property1 { get; set; }
  [JsonProperty("prop2")]
  public string Property2  { get; set; }
}

シリアライズ・デシリアライズを制御する

シリアライズ・デシリアライズのメソッドに、

する方法がある。

シリアライズ・デシリアライズの既定値を制御するオブジェクト

  • シリアライズ・デシリアライズ時に、
    シリアライズ・デシリアライズの既定値を制御する
    オブジェクト(JsonSerializerSettings)を指定できる。

    • シリアライズ時に指定

      string json = JsonConvert.SerializeObject(aaa, Formatting.Indented, new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate });
    • デシリアライズ時に指定

      AAA aaa = JsonConvert.DeserializeObject<UserModel>(jsonstring, new JsonSerializerSettings { DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate });
  • Handling

    • DefaultValueHandling
      • Include
        シリアライズ時に既定値の項目を JSON に含める(既定)。
      • Ignore
        シリアライズ時に既定値の項目を JSON に含めない。
      • Populate
        デシリアライズ時に JSON 文字列中に要素が存在しない場合でも既定値を設定。
      • IgnoreAndPopulate
        Ignore と Populate の同時指定。
    • NullValueHandling
      null の値をどう処理するか
      • Include
        オブジェクトをシリアライズおよびデシリアライズするときには、null 値を含めます。
      • Ignore
        オブジェクトのシリアライズおよびデシリアライズ時には null 値を無視します。
    • MissingMemberHandling
      デシリアライズ時、メンバが存在しない場合の動作
      • Ignore
        無視する(既定)
      • Error
        例外を出力
  • ContractResolver

    • DefaultContractResolver
      メンバ名は PascalCase で JSON 化される。
    • CamelCasePropertyNamesContractResolver
      メンバ名は CamelCase で JSON 化される。
  • Formatting

    • None
      インデントなし
    • Indented
      インデントをつける

補足(MissingMemberHandling の既定値が「無視」である意味): この既定値は
API のバージョン互換性に直結するため、意識して選ぶ必要がある。

【Ignore(既定)】
  サーバが新しい項目を追加した JSON を返す
     ↓ 古いクライアントは知らない項目を【黙って捨てる】
  → 前方互換性がある(壊れない)

【Error】
  知らない項目があると例外
     ↓
  → サーバが項目を 1 つ足しただけでクライアントが全滅する

つまり、既定の Ignore が API 連携では正しい
これは「知らないものは無視する」という
**堅牢性の原則(Postel の法則)**に沿った挙動である。

逆に、設定ファイルの読み込みなど
「タイプミスに気付きたい」場面では Error が適切になる。

なお、**System.Text.Json でも既定は「無視」**である。

シリアライズ・デシリアライズの既定値を制御する属性

[JsonObject("aaa")]
public class AAA
{
  [JsonProperty("prop1")]
  public int Property1 { get; set; }
  [JsonProperty("prop2")]
  [DefaultValue("hogehoge")]
  public string Property2  { get; set; }
  [JsonIgnore]
  public string Property3  { get; set; }
}
  • JsonProperty 属性には、更に様々なプロパティを設定可能。
[JsonProperty("prop2", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)]
[DefaultValue("hogehoge")]
public string Property2 { get; set; }

補足(JsonIgnore はセキュリティ上も重要): JsonIgnore
「不要な項目を省く」だけでなく、
機密情報を応答に漏らさないための手段でもある。

public class User
{
    public string Id { get; set; }
    public string Name { get; set; }

    [JsonIgnore]                      // ← これが無いと API 応答に載る
    public string PasswordHash { get; set; }
}

ただし、「付け忘れ」に依存する設計は危険である。
より確実なのは、

手段 内容
DTO を分ける エンティティをそのまま返さない。応答専用のクラスを作る
JsonIgnore 補助的に

エンティティ(DB のモデル)をそのまま API で返す設計は、
新しい列を追加した瞬間にそれが外部に露出するため、
公開 API では避けるのが原則である。

応用

上記のリンク先の記事に

「階層構造を持った JSON でも問題なくデシリアライズ可能。
JSON 配列も List などにパースしてくれる。」

とあるが、何処までやってくれるか?

・・・検証の結果、

  • 型が特定できる構造の JSON はシリアライズできる。
  • 型が特定できない構造の JSONは、シリアライズは可能だが、
    デシリアライズ結果は(インデクサが使用可能な)JObjectに格納される。

ということが解った。

以下は、型が特定できる構造の JSON を parse する例。

階層構造を持ったBean, POCOを使用する

  • 型を明示
    • Bean, POCO
[JsonObject("aaa")]
public class AAA
{
  [JsonProperty("prop1")]
  public int Property1 { get; set; }
  [JsonProperty("prop2")]
  public string Property2 { get; set; }
  [JsonProperty("prop3")]
  public AAA Property3 { get; set; }
}
  • シリアライズ・デシリアライズ
static void Main(string[] args)
{
  AAA aaa = new AAA();
  aaa.Property1 = 100;
  aaa.Property2 = "xxx";
  aaa.Property3 = new AAA();
  aaa.Property3.Property1 = 200;
  aaa.Property3.Property2 = "yyy";

  string json = JsonConvert.SerializeObject(aaa);
  Console.WriteLine(json);

  aaa = JsonConvert.DeserializeObject<AAA>(json);
  • 型を明示しない
    オブジェクト型を使用する(JObject 型にデシリアライズされる)。
    • Bean, POCO
[JsonObject("aaa")]
public class AAA
{
  [JsonProperty("prop1")]
  public int Property1 { get; set; }
  [JsonProperty("prop2")]
  public string Property2 { get; set; }
  [JsonProperty("prop3")]
  public object Property3 { get; set; }
}
  • シリアライズ・デシリアライズ
static void Main(string[] args)
{
  AAA aaa1 = new AAA();
  AAA aaa2 = new AAA();
  aaa1.Property1 = 100;
  aaa1.Property2 = "xxx";
  aaa2.Property1 = 200;
  aaa2.Property2 = "yyy";
  aaa1.Property3 = aaa2;

  string json = JsonConvert.SerializeObject(aaa1);
  Console.WriteLine(json);

  JObject jobj = (JObject)JsonConvert.DeserializeObject(json);

Primitive型とGeneric型を使用する

  • Primitive 型と Generic 型の範囲であれば、JSON の型が特定できるので、parse できる。
  • このため、Primitive 型と Generic 型の範囲で自由な構造を組むことができる。
  • しかし、Generic では、Bean, POCO を使用するため(フィールド名・フィールド型の組み合わせが)自由な構造を組めない。
static void Main(string[] args)
{
  List<Dictionary<string, string>> parent = new List<Dictionary<string, string>>();
  Dictionary<string, string> child;

  child = new Dictionary<string, string>();
  child["aaa"] = "AAA";
  child["bbb"] = "BBB";
  child["ccc"] = "CCC";
  parent.Add(child);

  child = new Dictionary<string, string>();
  child["xxx"] = "XXX";
  child["yyy"] = "YYY";
  child["zzz"] = "ZZZ";
  parent.Add(child);

  string json = JsonConvert.SerializeObject(parent);
  Console.WriteLine(json);

  parent = JsonConvert.DeserializeObject<List<Dictionary<string, string>>>(json);

Listを使用する

従って、List<POCO> 等と言った、Generic + POCO も問題なく処理できる。

  • 処理
    • Bean, POCO
[JsonObject("aaa")]
public class AAA
{
  [JsonProperty("prop1")]
  public int Property1 { get; set; }
  [JsonProperty("prop2")]
  public string Property2 { get; set; }
  [JsonProperty("prop3")]
  public AAA Property3 { get; set; }
}
  • シリアライズ・デシリアライズ
static void Main(string[] args)
{
 AAA aaa1 = new AAA();
 AAA aaa2 = new AAA();
 aaa1.Property1 = 100;
 aaa1.Property2 = "xxx";
 aaa2.Property1 = 200;
 aaa2.Property2 = "yyy";
 aaa1.Property3 = aaa2;

 List<AAA> lstaaa = new List<AAA>();
 lstaaa.Add(aaa1);
 lstaaa.Add(aaa2);

 string json = JsonConvert.SerializeObject(lstaaa);
 Console.WriteLine(json);

 lstaaa = JsonConvert.DeserializeObject<List<AAA>>(json);

継承クラスで基底クラスの型を使用する

  • 派生型をシリアライズできるが、問題は派生型にデシリアライズできないこと。
    (継承クラスのオブジェクトインスタンスを基底クラスでハンドルしようとした場合)
    • バイナリ・シリアライズではこういうことができたが、それは型情報を持たせているから。
    • 型情報を持たない JSON のシリアライズでは、こういうことはできない。
  • DataContractJsonSerializer では型情報の出力などもサポートされていたが、
    昨今の相互運用性が重視される JSON 利用方法を考えると、もう使われない機能になった。
  • 処理
    • Bean, POCO
[JsonObject("aaa")]
public class AAA
{
  [JsonProperty("prop1")]
  public int Property1 { get; set; }
  [JsonProperty("prop2")]
  public string Property2 { get; set; }
  [JsonProperty("prop3")]
  public AAA Property3 { get; set; }
}

[JsonObject("bbb")]
public class BBB : AAA
{
  [JsonProperty("prop4")]
  public int Property4 { get; set; }
}

[JsonObject("ccc")]
public class CCC : AAA
{
  [JsonProperty("prop4")]
  public string Property4 { get; set; }
}
  • シリアライズ・デシリアライズ
static void Main(string[] args)
{
  AAA aaa1 = new AAA();
  AAA aaa2 = new AAA();

  aaa1.Property1 = 100;
  aaa1.Property2 = "xxx";
  aaa1.Property3 = new BBB(); // AAAの派生のBBB

  aaa2.Property1 = 200;
  aaa2.Property2 = "yyy";
  aaa2.Property3 = new CCC(); // AAAの派生のCCC

  List<AAA> lstaaa = new List<AAA>();
  lstaaa.Add(aaa1);
  lstaaa.Add(aaa2);

  string json = JsonConvert.SerializeObject(lstaaa);
  Console.WriteLine(json);

  // BBBとCCCがAAA型のProperty3フィールドにデシリアライズされない。
  lstaaa = JsonConvert.DeserializeObject<List<AAA>>(json);

補足(この「派生型の問題」が本ページの核心のひとつ): 検証結果の
「型情報を持たない JSON のシリアライズでは、こういうことはできない」という
結論は正確であり、かつ重要な設計上の含意
を持つ。

【バイナリ シリアライズ(BinaryFormatter 等)】
  "型名を含む" ので、復元時に元のクラスに戻せる
     ↓ しかし
  【任意の型を復元できる = 攻撃者が任意の型を作らせられる】
     → 重大な脆弱性(デシリアライズ攻撃)

【JSON】
  型情報を持たない = 復元先の型は【受け取る側が決める】
     → 安全だが、派生型は復元できない

つまり、「できない」のは制約ではなく、安全側の設計である。
実際、BinaryFormatter は .NET 5 で非推奨、
.NET 9 で削除
された(デシリアライズ攻撃のため)。

どうしても多態性が必要な場合の手段は次のとおり。

手段 内容 安全性
判別子(discriminator)を自分で持つ {"type":"BBB", ...} を見て自分で分岐する 安全(推奨)
TypeNameHandling(JSON.NET) 型名を JSON に埋め込む 危険(任意の型を生成され得る)
[JsonDerivedType](.NET 7+) 許可する派生型を明示して多態性を実現 安全

TypeNameHandling.All既知の脆弱性であり、
外部から受け取る JSON には絶対に使ってはならない

現在は System.Text.Json[JsonDerivedType](.NET 7 以降)が
正しい答えで、許可する型を宣言的に限定したうえで
多態的なシリアライズ/デシリアライズができる。

型が特定できない構造のJSON

型が特定できない構造のJSONの例

シリアライズの対象に object 型を使用しており、

シリアライズの際、

  • 任意の型が指定される。
  • 出力される JSON の構造が一定ではない。

移行メモ(体裁): 原典の「出力されるされるJSON」は
「される」が重複していたため修正した。

object型を使用せざるを得ないケース

以下の様な構造が一定ではないケースでは、
シリアライズの対象に object 型を使用せざるを得ない。

  • 子要素 JSON の構造が一定ではない(Field に object 型を使用する)
  • 配列内の JSON の構造が一定ではない(object[], Hashtable を使用する)

JObjectのLINQ to JSONで手動のデシリアライズ

型が特定できない構造の(object 型をシリアライズした)JSON を
デシリアライズすると JSON データは JObject 型に格納される。

この場合、JSON.NET の JObject の LINQ to JSON で型の手動のデシリアライズを行う

参考

DataContractJsonSerializer

WCFの既定のシリアライザ。

流行ってないので基本スルーだが、
使う場合は、以下の点に注意する。

Javaや JSON.NET のシリアライザとの違い。

.NET の DataContractJsonSerializer は Java や JSON.NET と、シリアライザの挙動が違う。
そのため、同じ形式の POJO または POCO であっても、シリアライズ後の JSON フォーマットが異なる。

Dictionaryのシリアライズ結果

特に、以下のように Dictionary の JSON シリアライズ結果が異なる。

# 技術 POJO / POCO サンプル シリアライズされた JSON
1 Java, JSON.NET public class Sample {
  public String _key1;
  public String _key2;
  public void setKey1(String key1) { ... }
  public String getKey1() { ... }
  public void setKey2(String key2) { ... }
  public String getKey2() { ... }
}
{ "key1" : "value1", "key2" : "value2" }
2 同上 public class Sample {
  public HashMap<String, String> _key;
  public void setKey(HashMap<String, String> key) { ... }
  public void getKey() { ... }
}
{ "key1" : "value1", "key2" : "value2" }
3 .NET の DataContractJsonSerializer public class Sample {
  public string key1 { get; set; }
  public string key2 { get; set; }
}
{ "key1" : "value1", "key2" : "value2" }
4 同上 public class Sample {
  public Dictionary<string, string> key { get; set; }
}
[
  {
    "Key": "Prop1",
    "Value": "Value1"
  },
  {
    "Key": "Prop2",
    "Value": "Value2"
  }
]

同じ形式のジェネリック型であっても、
使用する Serializer 次第でシリアライズ結果が異なる

移行メモ(表の変換): 原典は PukiWiki のセル結合記法(~)で
「上のセルと同じ」を表現していたため、同じ値を展開して記載した。
また、Java 側の getter / setter の中身は
定型的な実装であったため { ... } に省略した。

補足(この表が示す問題の深刻さ): 4 行目が本ページで
最も実務的な発見である。

【Java / JSON.NET】
  Map / Dictionary → { "key1":"value1", "key2":"value2" }
                       ↑ 直感的。JSON オブジェクトになる

【DataContractJsonSerializer】
  Dictionary → [ {"Key":"key1","Value":"value1"},
                 {"Key":"key2","Value":"value2"} ]
                       ↑ KeyValuePair の【配列】になる(!)

これがなぜ起きるかというと、DataContractJsonSerializer
Dictionary<K,V> を「KeyValuePair<K,V> のコレクション」として
そのまま素直にシリアライズしている
ためである。
.NET の内部構造をそのまま JSON に写した結果であり、
JSON としては不自然だが、.NET としては一貫している

実務上の影響は深刻で、

場面 何が起きるか
Java 側と連携 同じ POCO/POJO なのに JSON が一致しない
JavaScript から利用 obj.key1 でアクセスできない(配列を走査する必要)
後からシリアライザを変更 JSON の形が変わり、既存クライアントが壊れる

これこそが
JSONを送信するRESTサービスを作成する方法
**「いきなり POCO をデータコントラクトとしない」**という主張の
根拠になっている実例である。

なお、System.Text.Json は Java / JSON.NET と同じ挙動
Dictionary を JSON オブジェクトにする)であるため、
現在この問題に当たるのは
WCF の既定設定を使っている場合に限られる。

Efficient dictionary serialization, Nonsensical dictionary serialization

この違いは、以下の機能のサポート状況によるものらしい。

System.Text.Json(.NET Core 3.0以降の標準)

概要

経緯を読んでみると、JSON.NETに何か問題があるという話ではなく、

  • JSON parse の組込サポートが無く、また、外部依存を外したかった。
  • MS が主体的に JSON の parse 処理の性能向上を図っていきたいと思っているらしい。
    • Span<T> を使用する。
    • UTF-16 に変換せず UTF-8 を直接処理する。
  • 確定的な動作を重視し(推測や解釈は行われない)、
    パフォーマンスとセキュリティを確保する。

と言う事の様です。

補足(3 つの理由のうち「外部依存を外したかった」が最も大きい): 経緯として
正確な整理だが、なぜ外部依存が問題だったのかを補足する。

【従来】ASP.NET Core が Newtonsoft.Json に依存
     ↓
  アプリも Newtonsoft.Json を使う
     ↓
  【バージョンの衝突】
    ・フレームワークが要求する版と、アプリが使いたい版が違う
    ・ライブラリごとに要求する版が違う

フレームワークが特定のサードパーティ製ライブラリに
依存していること自体
が、利用者にとっての制約になっていた。
標準ライブラリに取り込むことで、この問題が解消された。

また、性能面の理由も実際に大きい。

観点 Newtonsoft.Json System.Text.Json
文字列の扱い UTF-16 に変換してから処理 UTF-8 のまま処理
メモリ 文字列を都度確保 Span<T> で確保を減らす
速度 概ね 1.3〜2 倍程度速い

HTTP の本文は元々 UTF-8 であるため、
UTF-16 への変換自体が無駄だった、というのが要点である。

詳細

実装方法

..オイオイ書くカモ...

移行方法

補足(「推測や解釈関連の機能はサポートされない」の具体例): 未記載の
節だが、この一行が移行時の躓きどころを的確に予告している。

System.Text.Json厳格であり、
Newtonsoft.Json が「気を利かせて」やっていたことをしない。

挙動 Newtonsoft.Json System.Text.Json
プロパティ名の大文字小文字 区別しない(既定) 区別する(既定)※
数値を文字列で受け取る 変換する 例外(オプションで許可可)
末尾のカンマ、コメント 許容 例外(オプションで許可可)
TypeNameHandling あり 無し(安全側)
循環参照 設定で対応 ReferenceHandler で対応(.NET 5 以降)

※ ASP.NET Core の MVC では既定で大文字小文字を区別しない設定になる。

属性の対応は次のとおり。

Newtonsoft.Json System.Text.Json
[JsonProperty("name")] [JsonPropertyName("name")]
[JsonIgnore] [JsonIgnore]名前空間が違う
[JsonObject] 相当なし

[JsonIgnore] は名前が同じで名前空間が違うため、
using の付け替えを忘れると属性が効かなくなる
(コンパイルは通る)。移行時の典型的な事故である。

参考

JSON.parse、JSON.stringify(JavaScript)

JavaScript では、JSON.parse(シリアライズ)、JSON.stringify(デシリアライズ)が使用できる。

JSON.parse

JSON 文字列を解析して JavaScript のオブジェクトに変換する(シリアライズ)。

JSON.stringify

JavaScript の値を JSON 文字列に変換する(デシリアライズ)。

移行メモ(正誤): 原典は
**JSON.parse を「シリアライズ」、JSON.stringify を「デシリアライズ」**と
記載しているが、逆である

メソッド 動作 正しい呼称
JSON.parse 文字列 → オブジェクト デシリアライズ(復元)
JSON.stringify オブジェクト → 文字列 シリアライズ(直列化)

各節の説明文(「JSON 文字列を解析して〜変換する」等)は正しいため、
括弧内の用語のみが取り違えられている
原典どおりに移行したうえで、ここに訂正を記した。

参考

応用

コンボ生成でJSONを使用する際の注意事項(順番保証)。

順番保証がされないので、

  • Dictionary<string, string> ではなく、

    { "key1" : "value1", "key2" : "value2" }
  • List<List<String>> を使用する。

    [ [ "key1", "value1" ], [ "key2", "value2" ] ]

補足(この回避策の理由): 短いが、実務上きわめて有用な指摘である。

【問題】
  JSON のオブジェクトは、仕様上【キーの順序に意味が無い】
     ↓
  ・シリアライザが並べ替えるかもしれない
  ・JavaScript のオブジェクトも順序を保証しない(数値キーは特に)
     ↓
  コンボボックスの選択肢が【勝手に並び替わる】

【解決】
  配列は【順序に意味がある】
     ↓
  [ ["key1","value1"], ["key2","value2"] ] とすれば順序が保たれる

現在の書き方であれば、オブジェクトの配列にする方が読みやすい。

[ { "value": "1", "text": "選択肢A" },
  { "value": "2", "text": "選択肢B" } ]

「順序に意味があるものは配列で表す」という原則は、
コンボボックスに限らずJSON 設計の基本
である
(メニュー、手順、ランキングなども同様)。

JObjectを使って手動で任意の型にデシリアライズする。

JSON.NETの JObject では、

  • LINQ to JSON を使用して、任意のノードにアクセスできる。
  • LINQ to JSON と言っても、単純にインデクサ的に処理できる模様。

Deserialize

以下のようにデシリアライズできる。

JObject jObject = (JObject)JsonConvert.DeserializeObject(jsonResult);
if (jObject["Message"] != null)
{
    // 正常終了
    List<Dictionary<string, string>> list =
        JsonConvert.DeserializeObject<List<Dictionary<string, string>>>(jObject["Result"].ToString());
    this.lstRecords.ItemsSource = list;
    message = "正常終了しました";
}
else
{
    ・・・

Message の戻りによって、Result の方が変わる事例。

Serialize

上記のシナリオに合わせて、以下のようにシリアライズできる。

object ret = null;
if(・・・)
{
  ret = new { ErrorMSG = message };
}
else
{
  List<Dictionary<string, string>> list = new List<Dictionary<string, string>>();
  ・・・
  ret = new { Message = "", Result = list };
}
return Request.CreateResponse(HttpStatusCode.OK, ret);

単に object 型を使用したり、匿名型を使用したりして、
シリアライズする object 階層構造を可変にすればイイ。

補足(これが本ページの結論部分): 冒頭の
**「非構造化データを粘土細工のようにゴリゴリと処理したい」**という
要件に対する答えがここである。

【型が決まっている部分】     → POCO にデシリアライズ(型安全)
【型が決まっていない部分】   → JObject で受けて、インデクサで辿る
     ↓ 中身が判明した時点で
  JsonConvert.DeserializeObject<T>(jObject["Result"].ToString())
     ↓
  その部分だけ型付きで扱う

つまり、「全部を型で受ける」か「全部を動的に扱う」かの
二択ではなく、部分ごとに使い分けられる
というのが要点である。

なお、コード例では
jObject["Result"].ToString() で文字列に戻してから
再度デシリアライズ
しているが、
現在は ToObject<T>() を使う方が簡潔かつ効率的である。

var list = jObject["Result"].ToObject<List<Dictionary<string, string>>>();

System.Text.Json では JsonNode / JsonDocument
JObject に相当する。

JSON.NET System.Text.Json
JObject(可変) JsonNode / JsonObject(可変。.NET 6 以降)
JToken.ToObject<T>() JsonNode.Deserialize<T>()
JsonDocument読み取り専用・高速

読むだけなら JsonDocument(確保が少ない)、
組み立て・書き換えが要るなら JsonNode という使い分けになる。

なお、この設計(応答の形が状況で変わる)は
使い勝手はよいが、API の契約としては曖昧になる。
JSONを送信するRESTサービスを作成する方法
スキーマ ファーストの観点では、

  • 正常時と異常時で形を変えない(常に同じ枠を返す)、
  • 異常は HTTP のステータス コードで表す

方が、クライアントの実装が単純になる。

参考

参考


Tags: 移行, .NET開発, .NET Core, ASP.NET, ASP.NET Web API

NetDevInfraWiki

マイクロソフト系技術情報 Wiki
Open 棟梁 Wiki

(未着手)

開発基盤部会 Wiki

移行管理: DONETODO

Clone this wiki locally