C#
JSON

C# JSON Serialize

C# Serialize

JSONシリアライズを行っていて、いくつかつまずいた点があったので忘備録として書いています。

継承がある場合について追記する予定です。(2018.1.4)
追記しました。(2018.1.9)

開発環境

Vistual Studio 2017
.NET Framework 4.5.2

追加参照

  • System.Runtime.Serialization
    • 参照の追加で追加することが可能
  • Newtonsoft.Json
    • Newtonsoft.JsonはNuGetパッケージの管理でインストールすることが可能

実装

継承なしの場合

Jsonのシリアライズを行いクラスに以下の下準備を行う。

Classには[DataContract]属性を与える。
保存したいメンバー変数には[DataMember]属性を与える。
保存したくないメンバー変数には[IgnoreDataMember]`属性を与える。

注意しなければならないのは以下の通りである。
- デフォルト・コンストラクタは明示しないとシリアライズ時にエラーとなる。
- 保存したいメンバー変数のアクセサーは{ get; set; }にしなければならない。(public string Name;でもよい)

※もし保存したいがsetはしてほしくない場合はprivate変数にして、アクセス制御を行えばよい

Test.cs
[DataContract]
public class Test
{
    [DataMember]
    public string Name { get; set; }

    [IgnoreDataMember]
    public int Value { get; }

    public Test() { }

    public Test(int value)
    {
        Value = value;
    }
}

次にシリアライズ用の関数を提供するclassを作成する。
私の場合は次のようにファイルにまとめておいて、いろいろなプロジェクトで使えるようにしている。

JsonSerializer.cs
internal class JsonSerializer
{
    /// <summary>
    /// 通常用
    /// </summary>
    /// <typeparam name="TYpe">任意の型</typeparam>
    /// <returns></returns>
    public static DataContractJsonSerializer Serializer<TYpe>() => new DataContractJsonSerializer(typeof(TYpe));

    /// <summary>
    /// Listオブジェクト用
    /// </summary>
    /// <typeparam name="TYpe">任意の型</typeparam>
    /// <returns></returns>
    public static DataContractJsonSerializer SerializerList<TYpe>() => new DataContractJsonSerializer(typeof(List<TYpe>));

    /// <summary>
    /// Dictionaryオブジェクト用
    /// </summary>
    /// <typeparam name="TYpe1">任意の型</typeparam>
    /// <typeparam name="TYpe2">任意の型</typeparam>
    /// <returns></returns>
    public static DataContractJsonSerializer SerializerDictionary<TYpe1, TYpe2>() => new DataContractJsonSerializer(typeof(Dictionary<TYpe1, TYpe2>));
}

さてこれらを用意したところでJsonのRead及びWriteを行う。

Program.cs
partial class Program
{
    static void Main(string[] args)
    {
        var t = new Test();
        var list = new List<int>();
        var dic = new Dictionary<int, string>();

        //Read
        using (var fs1 = new FileStream("ファイル名1", FileMode.Open, FileAccess.Read))
        using (var fs2 = new FileStream("ファイル名2", FileMode.Open, FileAccess.Read))
        using (var fs3 = new FileStream("ファイル名3", FileMode.Open, FileAccess.Read))
        {
            t = (Test)JsonSerializer.Serializer<Test>().ReadObject(fs1);
            list = (List<int>)JsonSerializer.SerializerList<int>().ReadObject(fs2);
            dic = (Dictionary<int, string>)JsonSerializer.SerializerDictionary<int, string>().ReadObject(fs3);
        }

        //Write
        using (var fs1 = new FileStream("ファイル名1", FileMode.Create, FileAccess.Write))
        using (var fs2 = new FileStream("ファイル名2", FileMode.Create, FileAccess.Write))
        using (var fs3 = new FileStream("ファイル名3", FileMode.Create, FileAccess.Write))
        {
            JsonSerializer.Serializer<Test>().WriteObject(fs1, t);
            JsonSerializer.SerializerList<int>().WriteObject(fs2, list);
            JsonSerializer.SerializerDictionary<int, string>().WriteObject(fs3, dic);
        }
    }
}

これで
- 普通の型(自作クラスも含む)
- List型
- Dictionary型
の時のRead/Writeの方法の説明は以上である。

継承ありの場合

Userという抽象化されたクラスを定義する

User.cs
[DataContract]
public class User
{
    [DataMember]
    public string Name;
}

Userを継承したStudentクラスとTeacherクラスを作成する

Teacher.cs
[DataContract]
public class Teacher : User
{
    [DataMember]
    public int Salary;
}
Student.cs
[DataContract]
public class Student : User
{
    [DataMember]
    public string SchoolYear;
}

この状態で以下のコードを実行した場合例外が発生する。

Sample1.cs
internal class Program
{
    static void Main(string[] args)
    {
        var students = new List<Student>();
        var teachers = new List<Teacher>();
        var users = new List<User>();

        students.Add(new Student { Name = "田中", SchoolYear = "大2" });
        teachers.Add(new Teacher { Name = "鈴木", Salary = 10000000 });
        users.AddRange(students);
        users.AddRange(teachers);

        //Write
        using (var fs1 = new FileStream("Students.json", FileMode.Create, FileAccess.Write))
        using (var fs2 = new FileStream("Teachers.json", FileMode.Create, FileAccess.Write))
        using (var fs3 = new FileStream("Users.json", FileMode.Create, FileAccess.Write))
        {
            JsonSerializer.SerializerList<Student>().WriteObject(fs1, students);
            JsonSerializer.SerializerList<Teacher>().WriteObject(fs2, teachers);
            JsonSerializer.SerializerList<User>().WriteObject(fs3, users); // ここで例外
        }

        //Read
        using (var fs1 = new FileStream("Students.json", FileMode.Open, FileAccess.Read))
        using (var fs2 = new FileStream("Teachers.json", FileMode.Open, FileAccess.Read))
        using (var fs3 = new FileStream("Users.json", FileMode.Open, FileAccess.Read))
        {
            students = (List<Student>)JsonSerializer.SerializerList<Student>().ReadObject(fs1);
            teachers = (List<Teacher>)JsonSerializer.SerializerList<Teacher>().ReadObject(fs2);
            users = (List<User>)JsonSerializer.SerializerList<User>().ReadObject(fs3);  // ここで例外
        }
    }
}

コメントが書いてる箇所で例外が発生する。
ここでが発生する例外は
例外:System.Runtime.Serialization.SerializationException
コメント:DataContractSerializer を使用している場合は DataContractResolver を使用することを検討するか、静的に認知されていないすべての型を既知の型の一覧に追加してください。このためには、たとえば KnownTypeAttribute 属性を使用するか、シリアライザーへ渡される既知の型の一覧にこれらの型を追加します。
である。

そこでコメントに書いてあるKnownTypeAttributeを利用する。
そしてUserクラスを次のように編集する。

User.cs
[KnownType(typeof(Student))]
[KnownType(typeof(Teacher))]
[DataContract]
public class User
{
    [DataMember]
    public string Name;
}

UserクラスがTeacherクラスとStudentクラスを派生クラスであることを示すことができます。
ここで今一度Sample1を実行しても例外は発生しない。