Microsoft Graph .NET SDKでSharePointリストのfieldsを必要な列だけ取得する方法|$select/$expandとAdditionalData対策

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 のキー例)注意点
TitleTitle既定列。多くのケースでそのまま
Manage AccountManage_x0020_Account空白はエンコードされることがある
管理者(日本語列)Kanrisha / Kanrisha0 など作成時の内部名が英数字に自動変換されることも

API 側で列名をハードコードする場合、内部名をコードに固定するか、あるいは起動時に列一覧を取得してマッピングテーブルを作るなど、運用に合わせた工夫が必要です。

AdditionalData で出会いやすい値のパターン

AdditionalData の value は常に同じ型とは限りません。列の種類、値の入り方、シリアライザの違いで、string になったり JsonElement になったりします。代表例を押さえておくと実装が早いです。

列の種類Graph の返り方の例AdditionalData に入りやすい型実装のコツ
単一行テキスト“Title”: “A”string または JsonElement(String)GetString で吸収
数値“Count”: 10JsonElement(Number) / double / longje.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 と同じ「必要な列だけ」の返却仕様を安定して実現できます。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次