CodeGym /コース /C# SELF /属性でプロセスを制御する

属性でプロセスを制御する

C# SELF
レベル 45 , レッスン 2
使用可能

1. シリアライズで属性は何のためにあるのか?

オブジェクトを JSON や XML にシリアライズするとき、掃除ロボットが部屋をスキャンするようなことが起きます。目に見えるもの(public プロパティ/フィールド)は「バッグ」に入れられて(ファイルや文字列に出力され)それ以外は無視されます。でも時にはロボットに財布の中身を見せたくないとか、名前を別の呼び方にしたい(例えばロシア語じゃなくて英語にする)ことがあります。

そこで属性の出番です!属性を使うとシリアライズの挙動をコントロールできます:不要なものを隠す、名前を変更する、順序を付ける、オブジェクトの一部を無視する、あるいはシリアライザに特別なルールがあることを伝える。物に貼るラベルみたいなもので、「触るな」「重要」「JSON ではこれに名前を変える」みたいな指示を与えられます。

シリアライズ制御の典型的なタスク

  • 出力ドキュメントでプロパティやフィールドの名前を変える(例:JSON で FirstName の代わりに "first_name" にする)
  • シリアライズやデシリアライズから一部のフィールド/プロパティを隠す(例:パスワードや内部カウンター)
  • デフォルト値や null の扱いを管理する
  • 要素の順序を指定する(XML で重要)
  • XML 要素に追加のパラメーター(属性)を付ける

各シリアライザ — System.Text.JsonNewtonsoft.JsonXmlSerializer — はこれらを実現するためにそれぞれ固有の属性を使います。

2. System.Text.Json の属性:モダンで流行り

.NET の標準的な JSON シリアライザは、属性群をサポートしており、これらは名前空間 System.Text.Json.Serialization にあります。

よく使う属性:

属性 用途 使用例
JsonPropertyName("...")
JSON 内のプロパティ名を変換する
[JsonPropertyName("id")]
JsonIgnore
プロパティをシリアライズから完全に除外する
[JsonIgnore]
JsonInclude
public フィールドを(プロパティだけでなく)シリアライズ対象にする
[JsonInclude]
JsonIgnore(Condition = ...)
特定の条件でプロパティを無視する
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]

使用例:

using System.Text.Json.Serialization;

public class Person
{
    [JsonPropertyName("first_name")]
    public string FirstName { get; set; } // JSON では 'first_name' になる

    [JsonIgnore]
    public string Password { get; set; } // JSON に含まれない

    [JsonPropertyName("born_year")]
    public int? YearOfBirth { get; set; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string Nickname { get; set; } // null の場合は JSON に含まれない
}

このような人をシリアライズしてみましょう:

var person = new Person
{
    FirstName = "Ivan",
    Password = "123456",
    YearOfBirth = 2000,
    Nickname = null
};

string json = JsonSerializer.Serialize(person);
// json: {"first_name":"Ivan","born_year":2000}

注:パスワードもニックネームも JSON から消えています!(パスワードは常に無視され、ニックネームは null の場合に限り無視される)

なぜ重要か?実践ではクライアント側や攻撃者にパスワードやトークン、内部カウンター、内部タイムスタンプのような重要なデータを見せたくないことが多いです。[JsonIgnore] や類似の属性を使えば一行で対処できます。

3. Newtonsoft.Json の属性:柔軟性のフラグシップ

Newtonsoft.Json を使うと(ドキュメントは こちら)、さらに多くの機能が利用でき、古いプロジェクトに慣れている人には書きやすいこともあります。

主要な属性:

属性 用途
JsonProperty("...")
JSON 内の名前を指定したり、順序や必須指定などを行う
JsonIgnore
フィールド/プロパティをシリアライズから除外する
JsonRequired
デシリアライズ時に必須であることを要求する
JsonConverter
複雑なケースで独自のコンバーターを指定できる
DefaultValueHandling
デフォルト値のシリアライズ方法を制御する

実用例:

using Newtonsoft.Json;

public class UserProfile
{
    [JsonProperty("login")]
    public string Username { get; set; }

    [JsonIgnore]
    public string InternalNotes { get; set; }

    [JsonProperty(Required = Required.Always)]
    public string Email { get; set; }
}

JsonProperty の追加機能 — Required(必須)や Order(順序)などがあります。例えば:

[JsonProperty("id", Order = 1, Required = Required.Always)]
public int Id { get; set; }

4. XML 用の属性:クラシックなスタイル

XmlSerializer は名前空間 System.Xml.Serialization の属性群を使います。

よく見る属性:

属性 用途
[XmlElement("...")]
XML の要素名を別名にする
[XmlAttribute("...")]
プロパティを XML 要素の属性にする
[XmlIgnore]
プロパティやフィールドを XML シリアライズから除外する
[XmlArray("...")]
コレクション用に XML 配列の名前を指定する
[XmlArrayItem("...")]
コレクション要素の名前を指定する
[XmlRoot("...")]
ルート XML タグの名前を変更する

実践例:

using System.Xml.Serialization;

[XmlRoot("human")]
public class Person
{
    [XmlElement("firstname")]
    public string Name { get; set; }

    [XmlAttribute("years")]
    public int Age { get; set; }

    [XmlIgnore]
    public string Secret { get; set; }
}

var person = new Person { Name = "Anna", Age = 32, Secret = "42" };

シリアライズ後の XML はだいたいこんな感じになります:

<human years="32"><firstname>Anna</firstname></human>

フィールド Secret が XML に含まれておらず、Age は入れ子のタグではなく属性としてシリアライズされていることに注意してください。

5. 便利な注意点

内部でどう動いているか:属性はどう機能するか

シリアライザがクラスに出会うと、リフレクションで(.NET の魔法です。詳細は System.Reflection を見てください)クラスを読み取ります。シリアライザはメタデータを確認します:そのプロパティに JsonIgnoreXmlElement のような属性が付いているかどうか。属性に応じて、最終的なドキュメントにデータを追加したりスキップしたりします。

これは「データスキーマ」をビジネスロジックから分離する便利な方法です。あなたのクラスはビジネスロジックで、属性はシリアライズのパスポートみたいなものです。

主要属性の対応表

機能 System.Text.Json Newtonsoft.Json XmlSerializer
名前を変える
[JsonPropertyName]
[JsonProperty]
[XmlElement]
[XmlAttribute]
無視する
[JsonIgnore]
[JsonIgnore]
[XmlIgnore]
カスタムフォーマット
[JsonConverter]
[JsonConverter]
—(IXmlSerializable 経由、扱いづらい)
ルートオブジェクト
[XmlRoot]
コレクション
[XmlArray]
[XmlArrayItem]

6. 高度なシナリオ

標準のシリアライザがやる通りにオブジェクトをシリアライズしたくない場合があります。例えば日付を ISO 形式ではなく UNIX タイムスタンプで保存したいときなど。その場合は独自のコンバーターを接続できる属性があります。

System.Text.Json の場合:

public class UnixDateTimeConverter : JsonConverter<DateTime>
{
    public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => DateTimeOffset.FromUnixTimeSeconds(reader.GetInt64()).UtcDateTime;

    public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
        => writer.WriteNumberValue(new DateTimeOffset(value).ToUnixTimeSeconds());
}

public class LogEntry
{
    [JsonConverter(typeof(UnixDateTimeConverter))]
    public DateTime EventTime { get; set; }
}

これでシリアライズ時に EventTime は文字列ではなく数値(UNIX タイム)になります。

Newtonsoft.Json の場合:

public class BoolToYesNoConverter : JsonConverter<bool>
{
    public override void WriteJson(JsonWriter writer, bool value, JsonSerializer serializer)
        => writer.WriteValue(value ? "yes" : "no");

    public override bool ReadJson(JsonReader reader, Type objectType, bool existingValue, bool hasExistingValue, JsonSerializer serializer)
        => (string)reader.Value == "yes";
}

public class Answer
{
    [JsonConverter(typeof(BoolToYesNoConverter))]
    public bool IsCorrect { get; set; }
}

これで bool 値は "yes" または "no" としてシリアライズされ、逆に true/false に戻ります。

7. 特徴と落とし穴

属性を追加する際に覚えておくべきこと:シリアライザごとに使う属性が違います。JSON と XML の両方(あるいは Newtonsoft.JsonSystem.Text.Json を同時に使う)を扱う場合は、必要な属性を両方付け忘れないようにしてください。そうしないとびっくりする結果になります。

属性で上書きしない限り、デフォルトのプロパティ名がシリアライザによって使われます。

継承に注意:子クラスは親の public フィールド/プロパティを継承し、親に付いている属性も子クラスに適用されます。これは驚くことがありますが、仕様です。

よくあるミス:開発者が誤ってシリアライズに必須のフィールドに JsonIgnore を付けてしまったり、逆に機密データを無視するのを忘れたりすることが多いです。あるいは XmlElement を private フィールドに付けて「なぜシリアライズされないのか」と驚くケースがあります(XmlSerializer は public メンバーしか扱いません!)。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION