Microsoft Graph で SharePoint リストを読むとき、必要な列だけ返したいのに .NET の Graph SDK だと fields が AdditionalData に入り、null だらけの JSON になることがあります。本記事では原因と、DTO に組み立て直して最小形で返す実装パターンを具体例つきで解説します。
結論:SDK の ListItem をそのまま JSON 化しない
先に結論をまとめると、次の2点を押さえると要件どおりの「必要な列だけ」にかなり近づきます。
- Graph への要求(HTTP レベル)は $select と $expand で最小化する(これは REST でも SDK でも同じ)
- アプリから返す JSON は、ListItem をそのままシリアライズせず、Fields.AdditionalData から必要なキーだけ抜き出して DTO/匿名型で組み立て直す
「REST(Graph Explorer)では期待どおりの最小 JSON なのに、.NET SDK だと analytics など不要プロパティが大量に出る」「目的の列が fields 直下ではなく fields.additionalData に入る」という現象は、SDK が持つモデル(ListItem/FieldValueSet)の都合でほぼ説明できます。
よくある症状
- 不要なプロパティが大量(analytics など)が含まれ、ほとんど null
- 目的の列が fields の直下に見えない(実体は
Fields.AdditionalDataに入る) - 複数値列(例:ADGroup)が
{}のように見えて扱いづらい
REST と .NET SDK で見え方が変わる理由
まず前提として、Graph Explorer で $select/$expand を使ったときに「必要な列だけ」の JSON が返ってくるのは正常です。HTTP レベルでは、Graph は指定どおりのプロパティを返してくれます。
一方、.NET 側で GraphServiceClient を使うと、レスポンスは ListItem というクラスにマッピングされます。このクラスは Graph の仕様上あり得るプロパティをたくさん持っており、取得していないものは null になります。つまり、
- Graph のレスポンスは最小でも、
- C# のオブジェクトをそのまま JSON 化すると、null のプロパティまで出力されがち
というズレが起こります。
| 観点 | REST(Graph Explorer) | .NET Graph SDK(ListItem をそのまま JSON 化) |
|---|---|---|
| 取得するデータ | $select/$expand で指定した分だけ | HTTP レベルは同じでも、クラスの全プロパティが存在し得る |
| 不要プロパティ | 返ってこない | null としてプロパティが並びやすい(シリアライザ設定次第) |
| SharePoint の列 | fields オブジェクトのプロパティとして見える | 動的フィールドなので Fields.AdditionalData に格納される |
SharePoint の列は SDK では AdditionalData に入る
SharePoint リストの列(Title、カスタム列など)は、Graph の型として事前に固定できないケースが多い(リストごとに列が違う)ため、SDK では open type(動的プロパティ)として扱われます。
具体的には、ListItem の Fields は FieldValueSet 型で、SDK が「このプロパティ名は決め打ちできない」と判断するフィールド値は、FieldValueSet.AdditionalData(辞書)に格納されます。したがって、
item.Fields.Titleのような強い型付けでは取れないitem.Fields.AdditionalData["Title"]のように辞書経由で取る
という設計になります。
まずは SDK 側でもクエリを最小化する
「不要プロパティが大量に出る」問題の本丸はシリアライズですが、それでも HTTP レベルでの取得量は減らすべきです。SDK でも REST と同じ発想で $select/$expand を設定できます。
ListItem 一覧を取得し、fields だけ展開して必要列に絞る
var response = await graphClient
.Sites[siteId]
.Lists[listId]
.Items
.GetAsync(rc =>
{
// ListItem 側は id と fields だけ欲しい
rc.QueryParameters.Select = new[] { "id", "fields" };
// fields は展開して、欲しい列だけ select する
rc.QueryParameters.Expand = new[]
{
"fields($select=Title,ManageAccount,ADGroup)"
};
});
// response.Value が ListItem の配列
この時点で Graph から受け取る JSON 自体はかなり小さくなります。ところが、ここで response や response.Value をそのまま JSON にして API から返すと、ListItem クラスが持つ全プロパティが列挙され、null だらけの出力になりがちです。
不要プロパティが大量に出る原因と、見た目を軽くする方法
ASP.NET Core などで既定のシリアライザ設定のまま return Ok(response.Value); のように返すと、null のプロパティも出力されるケースがあります。これを「見た目だけ」軽くするなら、null を出力しない設定が効きます。
System.Text.Json で null を出力しない
builder.Services
.AddControllers()
.AddJsonOptions(o =>
{
o.JsonSerializerOptions.DefaultIgnoreCondition =
System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull;
});
Newtonsoft.Json を使う場合
builder.Services
.AddControllers()
.AddNewtonsoftJson(o =>
{
o.SerializerSettings.NullValueHandling =
Newtonsoft.Json.NullValueHandling.Ignore;
});
ただし、これは「null を隠す」だけです。fields が AdditionalData に入る構造自体は変わりません。最終的に Graph Explorer と同じような形(fields の中に必要なキーだけが並ぶ形)で返したいなら、次の DTO 方式が確実です。
実務で確実な回避策:AdditionalData から DTO を組み立てる
Graph SDK のモデル形状に期待するのではなく、アプリの返却仕様に合わせた DTO を作り、必要なキーだけ抽出して組み立て直すのが最短です。ポイントは3つです。
- AdditionalData のキーは 列の内部名(Name)になる
- 値は
objectとして入るため、型変換が必要 - 列が欠ける可能性もあるので、TryGetValue で安全に読む
匿名型でそのまま返す最小例
var minimized = response?.Value?.Select(item => new
{
id = item.Id,
fields = new
{
Title = item.Fields?.AdditionalData != null
&& item.Fields.AdditionalData.TryGetValue("Title", out var t) ? t : null,
ManageAccount = item.Fields?.AdditionalData != null
&& item.Fields.AdditionalData.TryGetValue("ManageAccount", out var m) ? m : null,
ADGroup = item.Fields?.AdditionalData != null
&& item.Fields.AdditionalData.TryGetValue("ADGroup", out var g) ? g : null,
}
});
// minimized を JSON 化して返す(null を省く設定にするとさらにスリム化できる)
return Ok(minimized);
この方法なら「ListItem の null プロパティ大量問題」を完全に回避できます。返す JSON はあなたが組み立てた匿名型(または DTO)だけになるためです。
DTO を作って明示的に型を整える例
匿名型でも良いのですが、運用が長い API なら DTO を作っておくと、クライアント側の期待が安定します。
public sealed record ListItemDto(
string Id,
string? Title,
string? ManageAccount,
IReadOnlyList<string> ADGroup
);
次に、AdditionalData から型を整えながらマッピングします。複数値列が混ざると object の中身が JsonElement になりやすいので、そこを丁寧に扱います。
static string? GetString(FieldValueSet? fields, string key)
{
if (fields?.AdditionalData == null) return null;
if (!fields.AdditionalData.TryGetValue(key, out var value) || value == null) return null;
// 文字列そのもの
if (value is string s) return s;
// JsonElement から文字列に
if (value is System.Text.Json.JsonElement je)
{
if (je.ValueKind == System.Text.Json.JsonValueKind.String) return je.GetString();
return je.ToString(); // 数値や true/false などは文字列化して返す方針もあり
}
return value.ToString();
}
static IReadOnlyList<string> GetStringArray(FieldValueSet? fields, string key)
{
if (fields?.AdditionalData == null) return Array.Empty<string>();
if (!fields.AdditionalData.TryGetValue(key, out var value) || value == null)
return Array.Empty<string>();
// すでに string[] / List<string> として入るケース
if (value is IEnumerable<string> strs) return strs.ToArray();
// JsonElement(Array) として入るケース
if (value is System.Text.Json.JsonElement je)
{
if (je.ValueKind == System.Text.Json.JsonValueKind.Array)
{
var list = new List<string>();
foreach (var e in je.EnumerateArray())
{
if (e.ValueKind == System.Text.Json.JsonValueKind.String)
list.Add(e.GetString() ?? "");
else
list.Add(e.ToString());
}
return list;
}
}
// 想定外は単一要素として扱う(必要に応じて方針変更)
return new[] { value.ToString() ?? "" };
}
var dtos = response?.Value?.Select(item =>
new ListItemDto(
Id: item.Id ?? "",
Title: GetString(item.Fields, "Title"),
ManageAccount: GetString(item.Fields, "ManageAccount"),
ADGroup: GetStringArray(item.Fields, "ADGroup")
)
) ?? Enumerable.Empty<ListItemDto>();
return Ok(dtos);
Graph Explorer と同じ「id + fields」形で返したい場合
クライアント側が Graph Explorer の出力に近い形を期待しているなら、DTO も id と fields の入れ子構造に合わせると混乱が減ります。
public sealed record ListItemMinDto(string Id, ListItemFieldsDto Fields);
public sealed record ListItemFieldsDto(
string? Title,
string? ManageAccount,
IReadOnlyList<string> ADGroup
);
var minimizedDtos = response?.Value?.Select(item => new ListItemMinDto(
Id: item.Id ?? "",
Fields: new ListItemFieldsDto(
Title: GetString(item.Fields, "Title"),
ManageAccount: GetString(item.Fields, "ManageAccount"),
ADGroup: GetStringArray(item.Fields, "ADGroup")
)
));
return Ok(minimizedDtos);
返却例は次のようなイメージになります(null 出力を無視する設定を入れると、欠けた列は自然に省かれます)。
{
"id": "12",
"fields": {
"Title": "案件A",
"ManageAccount": "[email protected]",
"ADGroup": ["Sales", "Tokyo"]
}
}
ADGroup が {} に見えるときの読み解き方
デバッグ表示やログで ADGroup が {} のように見えるとき、実体は次のどちらかであることが多いです。
- JsonElement の Object:中にプロパティを持つ(が、ToString/表示のされ方で空に見える)
- JsonElement の Array:複数値列だが、扱う側が配列として解釈していない
まずは value の型を確認します。AdditionalData の value が JsonElement なら、ValueKind を見れば配列かオブジェクトかが一発です。
if (item.Fields?.AdditionalData?.TryGetValue("ADGroup", out var g) == true && g is JsonElement je)
{
Console.WriteLine(je.ValueKind); // Array / Object / String ...
}
もし Object で返ってくる列(例:ユーザー列、ルックアップ列、管理メタデータ列など)なら、必要なプロパティだけ抜き出して DTO に落とすのが扱いやすいです。例えば displayName や email など、運用上必要な属性だけを返すようにします。
static IReadOnlyList<string> GetObjectArrayProperty(
FieldValueSet? fields,
string key,
string propertyName)
{
if (fields?.AdditionalData == null) return Array.Empty<string>();
if (!fields.AdditionalData.TryGetValue(key, out var value) || value is not JsonElement je)
return Array.Empty<string>();
if (je.ValueKind != JsonValueKind.Array) return Array.Empty<string>();
var list = new List<string>();
foreach (var obj in je.EnumerateArray())
{
if (obj.ValueKind != JsonValueKind.Object) continue;
if (obj.TryGetProperty(propertyName, out var p))
{
list.Add(p.ValueKind == JsonValueKind.String ? (p.GetString() ?? "") : p.ToString());
}
}
return list;
}
ADGroup が「グループ列(人/グループ)」や「ルックアップ列(複数)」の場合、このような抽出が効きます。列の種類によって Graph が返す形が変わるため、一度だけ実データの JSON をログ出力して形を確定し、その形に合わせた変換関数を作るのが安定します。
列の内部名を間違えると、AdditionalData に出てこない
AdditionalData のキーに使われるのは、基本的に SharePoint 列の「表示名」ではなく「内部名(name)」です。たとえば列の表示名が「Manage Account」でも、内部名は Manage_x0020_Account のようになることがあります。さらに列を後からリネームしても内部名は変わりません。
「$select したのに値が出てこない」「AdditionalData に存在しない」というときは、まず内部名が合っているかを疑います。
Graph で列一覧を取得して内部名を確認する
var columns = await graphClient
.Sites[siteId]
.Lists[listId]
.Columns
.GetAsync(rc =>
{
rc.QueryParameters.Select = new[] { "name", "displayName" };
});
foreach (var c in columns?.Value ?? Enumerable.Empty())
{
Console.WriteLine($"{c.DisplayName} => {c.Name}");
}
| 表示名(例) | 内部名(Graph のキー例) | 注意点 |
|---|---|---|
| Title | Title | 既定列。多くのケースでそのまま |
| Manage Account | Manage_x0020_Account | 空白はエンコードされることがある |
| 管理者(日本語列) | Kanrisha / Kanrisha0 など | 作成時の内部名が英数字に自動変換されることも |
API 側で列名をハードコードする場合、内部名をコードに固定するか、あるいは起動時に列一覧を取得してマッピングテーブルを作るなど、運用に合わせた工夫が必要です。
AdditionalData で出会いやすい値のパターン
AdditionalData の value は常に同じ型とは限りません。列の種類、値の入り方、シリアライザの違いで、string になったり JsonElement になったりします。代表例を押さえておくと実装が早いです。
| 列の種類 | Graph の返り方の例 | AdditionalData に入りやすい型 | 実装のコツ |
|---|---|---|---|
| 単一行テキスト | “Title”: “A” | string または JsonElement(String) | GetString で吸収 |
| 数値 | “Count”: 10 | JsonElement(Number) / double / long | je.GetInt32() などに寄せるか、文字列化で返す |
| Choice(単一) | “Status”: “Open” | string | ほぼそのまま |
| Choice(複数) | “Tags”: [“A”,”B”] | JsonElement(Array) / IEnumerable<string> | GetStringArray を用意 |
| 人/グループ(単一・複数) | 文字列 or オブジェクト配列 | JsonElement(Object/Array) | 必要な属性だけ抽出して DTO 化 |
| ルックアップ(単一・複数) | 数値 or オブジェクト配列 | JsonElement(Object/Array) | ID だけ返すのか表示値も返すのか方針を決める |
| 管理メタデータ | オブジェクト/配列になりやすい | JsonElement(Object/Array) | 運用で必要なラベルだけ返すのが無難 |
「Graph Explorer の JSON をそのまま返したい」場合の選択肢
API の仕様として「Graph Explorer と同じ JSON 形状を返す」ことが最優先で、DTO を定義するのも避けたい場合は、SDK のモデルに乗せずに生の JSON として扱う方法もあります。たとえば、Graph SDK のリクエストアダプタや HttpClient を使い、レスポンスを文字列として受け取ってそのまま返すイメージです。
| 方法 | メリット | デメリット | 向いているケース |
|---|---|---|---|
| DTO/匿名型で組み立て直す | 返却仕様が安定し、不要データを確実に排除できる | 変換コードが必要 | 自社 API として長期運用する |
| null を無視する設定だけ入れる | 実装が最短 | fields.additionalData の構造は変わらない | 社内ツールなど、出力形状を厳密に求めない |
| 生 JSON を返す | REST と同じ形を保ちやすい | 型安全が落ちる、エラー処理が増える | プロキシ的に Graph を中継したい |
個人的なおすすめは DTO 方式です。SharePoint リスト列は追加・変更されやすく、列タイプも混在しがちです。そのため、アプリ側で「この API はこの形を返す」と宣言し、必要な列だけを責任を持って返す方が、運用コストが下がります。
最小化を安定させるチェックリスト
- $select と $expand で Graph から受け取るデータを減らす
- AdditionalData のキーは 内部名で指定する(列一覧で確認する)
- API の返却は ListItem を直接返さず DTO にする
- 複数値列・参照列は JsonElement を前提に変換関数を用意する
- 見た目も軽くしたいなら null を出力しないシリアライザ設定を入れる
まとめ
Microsoft Graph の SharePoint リスト取得で「必要な列だけ返したい」のに .NET Graph SDK が期待どおりに見えないのは、Graph の問題というより、SDK のモデル化とシリアライズの都合が原因です。HTTP レベルでは $select/$expand で最小化できる一方、SharePoint の列は動的なので AdditionalData に入ります。
実務的には、AdditionalData から必要キーだけ抽出して DTO を組み立て直すのが最も確実で、REST と同じ「必要な列だけ」の返却仕様を安定して実現できます。

コメント