4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

C#でJsonをSerialize/Deserializeする方法

Last updated at Posted at 2020-05-02

実行環境

VS2019 Version 16.4.5
.NET Framework 4.7.2

使用するSerializer

  • DataContractJsonSerailizer
    • System.Runtime.Serializationを参照追加すれば使用可能。

NuGetで優秀なライブラリが数多く配布されてはいますが、
ここでは基本的なDataContractJsonSerializerを使用して分かりやすく説明します。

実装方法

まずはSerialize/Deserializeするためのシンプルなコード例を示します。

###実装コード(シンプル)

Program.cs
public class Test
{
    public string PublicStringProperty { get; set; } = "Public String Property";
    public string PublicStringField = "Public String Field";
    string PrivateStringProperty { get; set; } = "Private String Property";
    string PrivateStringField = "Private String Field";
}

class Program
{
    static void Main(string[] args)
    {
        var test = new Test();

        using (var fs = new FileStream("test.json", FileMode.Create))
        {
            var serializer = new DataContractJsonSerializer(typeof(Test));
            serializer.WriteObject(fs, test);
        }

        using (var fs = new FileStream("test.json", FileMode.Open))
        {
            var serializer = new DataContractJsonSerializer(typeof(Test));
            var dest = serializer.ReadObject(fs);
        }
    }
}

###出力結果

test.json
{"PublicStringField":"Public String Field","PublicStringProperty":"Public String Property"}

見て分かる通り、publicなプロパティとフィールドだけ出力されています。privateなメンバは出力されません。

また[IgnoreDataMember]でマークすると、そのメンバだけSerialize/Deserializeできないようになります。

IgnroeDataMemberの例
[IgnoreDataMember] // Serialize/Deserializeされなくなる
public string PublicStringProperty { get; set; } = "Public String Property";

プロパティの場合、プロパティ本体がpublic指定でも、get/setどちらかがpublic以外(internal/protected/private)になるとSerialize/Deserializeできなくなります。

プロパティのアクセス指定によるSerialize/Deserialize
public class Test
{
    // Serialize/Deserializeされる
    public int Value0 { get; set; } 
    
    // Serialize/Deserializeされない
    public int Value1 { get; internal set; } 
    public int Value2 { internal get; set; }
    public int Value3 { get; protected set; } 
    public int Value4 { protected get; set; }
    public int Value5 { get; private set; } 
    public int Value6 { private get; set; }
}

 
さて、出力できましたが改行されていないのでjsonコードが見づらいです。
段落及びインデントで整形してSerialize/Deserializeできるようにします。

###実装コード(整形あり)

using (var fs = new FileStream("test.json", FileMode.Create))
{
    var serializer = new DataContractJsonSerializer(typeof(Test));
    var jsonWriter = JsonReaderWriterFactory.CreateJsonWriter(fs, Encoding.UTF8, true, true, "    ");
    serializer.WriteObject(jsonWriter, test);
    jsonWriter.Dispose();
}

using (var fs = new FileStream("test.json", FileMode.Open))
{                
    var serializer = new DataContractJsonSerializer(typeof(Test));
    var jsonReader = JsonReaderWriterFactory.CreateJsonReader(fs, XmlDictionaryReaderQuotas.Max);
    var dest = serializer.ReadObject(jsonReader);
    jsonReader.Dispose();
}

###出力結果

test.json
{
    "PublicStringField": "Public String Field",
    "PublicStringProperty": "Public String Property"
}

jsonWriterDispose実行を忘れないよう気を付けてください。
Dispose処理時にFileStreamを経由して書き込まれるため)

usingで書き込めば安全に出力することができます。

using (var jsonWriter = JsonReaderWriterFactory.CreateJsonWriter(fs, Encoding.UTF8, true, true, "    "))
{
    serializer.WriteObject(jsonWriter, test);
}

JsonReaderWriterFactory.CreateJsonWriterの第5引数がインデントに代わる文字です。
半角スペース4文字を設定しています。

XmlDictionaryWriterJsonReaderWriterFactory.CreateJsonWriterで生成したWriter)経由で出力したデータは、記載したコードのようにXmlDictionaryReader経由で読み取るようにしてください。それ以外の方法で読み込むと例外が発生します。

###DataContractAttributeについて
先程Serialize/Deserializeするクラスには何もAttributeをマークしていませんでした。
よく、DataContractAttributeをマークしてSerialize/Deserializeする必要がある、と見聞きしますがそんなことはありません。

DataContractはあくまで明示することにより、Serialize/Deserializeに便利な挙動を提供できる仕組みでしかありません。(Serialize/Deserializeするプロパティ名を変えたり、Serialize/Deserializeの順番を変えたり等)

ここでは、DataContract及びDataMemberをマークした場合の挙動について説明します。

###実装コード(DataContractを使用した場合)

DataContractを使用した例
[DataContract]
public class Test
{
    [DataMember]
    public string PublicStringProperty { get; set; } = "Public String Property";

    public string PublicStringField = "Public String Field";

    string PrivateStringProperty { get; set; } = "Private String Property";

    [DataMember]
    string PrivateStringField = "Private String Field";
}

###出力結果

test.json
{
    "PrivateStringField": "Private String Field",
    "PublicStringProperty": "Public String Property"
}

[DataContract]でマークしたクラスは、[DataMember]でマークしたメンバのみSerialize/Deserializeされます。privateであっても、Serialize/Deserializeすることが可能です。

その他、引数付きコンストラクタのみを実装したクラスでもSerialize/Deserialize可能になります。

[DataContract]をマークしていない場合、
引数付きコンストラクタのみ実装されたクラスはSerialize/Deserializeできません。
(デフォルトコンストラクタを実装すればSerialize/Deserializeできます)

// Serialize/Deserialize可能
[DataContract]
public class Test
{
    public Test(int value){}
}

// Serialize/Deserializeで例外が発生する
public class Test
{
    public Test(int value){}
}

// Serialize/Deserialize可能
public class Test
{
    public Test(){}
    public Test(int value){}
}

###DataContractの注意点
[DataContract]でマークすると、引数付きコンストラクタのみでもSerialize/Deserialize可能でした。
逆に言えば、デフォルトコンストラクタが動作していないということが説明できます。

これは[DataContract]でマークしたクラスは、Deserialize時に対象クラスをFormatterServices.GetUninitializedObject(Type)を使用してオブジェクト生成されるからです。

[DataContract]
public class Test
{
	[DataMember]
	public string PublicStringProperty { get; set; } = "この文字で初期化されない";
}

Deserialize時に、ある特定の値に初期化しておきたい場合は、[OnDeserializing]をマークしたメソッドを実装します。

[DataContract]
public class Test
{
	[DataMember]
	public string PublicStringProperty { get; set; };
	
    [OnDeserializing]
    void DefaultDeserializing(StreamingContext sc)
    {
    	PublicStringProperty = "Public String Property";
    }
}

基本的にコード上でクラス生成されるので、デフォルトコンストラクタが動作して初期化されるとは思います。
しかし、後述する未初期化ケースによって問題になることがあります。(ありました)

###問題パターン
Test.Valueは、エディター上で3の値に編集されて、test.jsonとして出力されたとします。

出力する型
[DataContract]
public class Test
{
    [DataMember]
    public int Value; // 3が入る
}
test.json
{
    "Value": 3
}

仕様変更が入り、Test.ValueTest.Value2と名前が変更されたとします。
役割が同じということだったので、初期値は3が入るようにコードを修正します。

仕様変更後の型
[DataContract]
public class Test
{
    [DataMember]
    public int Value2 = 3;
}

この状態でtest.jsonをDeserializeするとどうなるでしょう。
test.jsonには、Valueというフィールドが存在しないため、Value2の読み込みは行われません。

そしてデフォルトコンストラクタも動作しないので、
このValue2は3で初期化されず、intの規定値となる0で初期化されます。

仕様変更後の型で仕様変更前のデータをDeserializeする際、こういった不具合の発生原因を作ってしまいます。

この場合、対応方法は各種ありますが、DataContractを使用して仕様変更前のデータをDeserializeしたいのであればDataMember(Name = "") で以前のフィールド名(プロパティ名)指定してあげれば、Deserializeすることが可能です。

仕様変更前のtest.jsonをDeserializeするためのコード
[DataContract]
public class Test
{
    [DataMember(Name = "Value")]
    public int Value2;
}

このような部分が、DataContractのメリットでもあり、デメリットでもあります。

###Serializeパターン
では、下記のようなパターンはSerialize/Deserializeできるでしょうか。

public class Child
{
    public int Value = 0;
}

[DataContract]
public class Parent
{
    [DataMember]
    public Child ChildValue = new Child();
}
parent.json
{
    "ChildValue": {
        "Value": 0
    }
}

Rootとなる型だけ、DataContractをマークしておけばSerialize/Deserialize可能です。
 
では、継承を用いた少し複雑なパターンはどうでしょうか。
(説明に不要なコードは、使用しないように修正しています)

Program.cs
[DataContract]
public class Super
{
    [DataMember]
    public int SuperValue = 0;
}

[DataContract]
public class Sub : Super
{
    [DataMember]
    public int SubValue = 1;
}

class Program
{
    static void Main(string[] args)
    {
        Sub instance = new Sub();

        using (var fs = new FileStream("sub.json", FileMode.Create))
        {
            var serializer = new DataContractJsonSerializer(typeof(Super));
            serializer.WriteObject(fs, instance);
        }

        using (var fs = new FileStream("sub.json", FileMode.Open))
        {
            var serializer = new DataContractJsonSerializer(typeof(Super));
            var dest = serializer.ReadObject(fs);
        }
    }
}

残念ながら、例外が発生します。(System.Runtime.Serialization.SerializationException)
基底クラス(Super)としてSerializeするはずが、型を知らない派生クラス(Sub)でSerializeしているからです。もちろん、Sub intanceの宣言をSuper instanceにしても、例外は発生します。

派生クラスの型さえ伝えられれば、たとえ基底クラスの型を指定していてもSerializeが可能になります。
(当たり前ですが、継承関係にあることが前提です)

DataContractJsonSerializerSettingsを使用する例
Sub instance = new Sub();

var settings = new DataContractJsonSerializerSettings()
{
    KnownTypes = new Type[] { typeof(Sub) }
};

using (var fs = new FileStream("sub.json", FileMode.Create))
{
    var serializer = new DataContractJsonSerializer(typeof(Super), settings);
    serializer.WriteObject(fs, instance);
}

using (var fs = new FileStream("sub.json", FileMode.Open))
{
    var serializer = new DataContractJsonSerializer(typeof(Super), settings);
    var dest = serializer.ReadObject(fs);
}

DataContractJsonSerializerSettings.KnownTypesに、継承関係にあるSubのTypeを配列で入れます。
これにより、DataContractJsonSerializerに基底クラスの型を指定しても、継承されたクラスのインスタンスをSerialize/Deserializeすることが可能になります。

###DataContractJsonSerializerSettingsについて
KnownTypes以外にも設定できる項目があります。
DataContractJsonSerializerSettings.EmitTypeInformationEmitTypeInformation.Alwaysを指定すれば、型情報も出力されるようになります。

Program.cs
namespace CSharp_sandbox
{
    [DataContract]
    public class Test
    {
        [DataMember]
        public int Value = 1;
    }

    class Program
    {
        static void Main(string[] args)
        {
            var test = new Test();

            var settings = new DataContractJsonSerializerSettings()
            {
                EmitTypeInformation = EmitTypeInformation.Always
            };

            using (var fs = new FileStream("test.json", FileMode.Create))
            {
                using (var writer = JsonReaderWriterFactory.CreateJsonWriter(fs, Encoding.UTF8, true, true, "    "))
                {
                    var serializer = new DataContractJsonSerializer(typeof(Test), settings);
                    serializer.WriteObject(writer, test);
                }
            }



            using (var fs = new FileStream("test.json", FileMode.Open))
            {
                using (var reader = JsonReaderWriterFactory.CreateJsonReader(fs, XmlDictionaryReaderQuotas.Max))
                {
                    var serializer = new DataContractJsonSerializer(typeof(Test), settings);
                    var dest = serializer.ReadObject(reader);
                }
            }
        }
    }
}
test.json
{
    "__type": "Test:#CSharp_sandbox",
    "Value": 1
}

型名:#名前空間という形で出力されます。

DataContractJsonSerializerSettings.UseSimpleDictionaryFormattrueにすると、
Dictionary<TKey,TValue>をSerializeする時のデータ形式が変わります。

var settings = new DataContractJsonSerializerSettings()
{
    UseSimpleDictionaryFormat = true
};

trueの場合

UseSimpleDictionaryFormat=true
{
    "Dict": {
        "TestKey0": 0,
        "TestKey1": 1,
        "TestKey2": 2
    }
}

falseの場合(規定値)

UseSimpleDictionaryFormat=false
{
    "Dict": [
        {
            "Key": "TestKey0",
            "Value": 0
        },
        {
            "Key": "TestKey1",
            "Value": 1
        },
        {
            "Key": "TestKey2",
            "Value": 2
        }
    ]
}

名前の通り、Key:Valueという形でシンプルにSerializeされるようになります。

#終わりに
自分でもたまにDataContractについて混乱することがあったため、検証がてら記事にしてみました。
この記事が誰かの助けになれば幸いです。

間違い等があれば、ご指摘していただけると助かります。

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?