LoginSignup
10
15

More than 3 years have passed since last update.

C#でYamlファイルをシリアライズ/デシリアライズする

Last updated at Posted at 2021-06-06

概要

早速ですがYamlファイルで初期設定したいですよね。少なくとも私はそうでした.

C#で使う方法としてはNuGetで配布されているYamlDotNetというパッケージを使うのが一番手っ取り早そうだったので今回触ってみたところ、参考資料が少なくて少し不便だったので備忘録も兼ねて今回共有できたらなと思いました。

ドキュメントとか用意してくれてるんですけど少し古いっぽくてエラー出てきちゃったんですよね。。。

元も子もないんですけど、一応最新のはずのGithubのリンクを貼っておきます。これを読んだらこんな記事読む必要無いです💦( https://github.com/aaubry/YamlDotNet

それでは書いていきます!

環境

Windows10
Visual Studio 2019
.NetCore 3.1.0
YamlDotNet 11.1.1

準備

YamlDotNetのインストール

  • 「プロジェクト」タブ(①)→「NuGetパッケージの管理」(②)と選択
    image.png

  • 「参照」タブを選択して、検索ボックスに「YamlDotNet」と入力

  • ①、②の順にクリックしてインストールすれば完了、インストール済みのところにYamlDotNetが追加されているはず
    image.png

YamlDotNetについて

チートシートを書きたいわけではない(書けるはずもない)のでここでは今回実装するにあたって知っておけば良さそうな情報をここでは共有します。

YamlDotNet.Serialization

  • Serializer.Serialize
    TextWriterとシリアライズしたい情報をもったオブジェクトを渡すと、TextWriterで設定されている出力先パスにファイルを作成してくれます。また、オブジェクトのみを渡すとStringが返ってきて、オブジェクトの中身を見れる便利機能もありました。

  • Deserializer.Deserialize
    ジェネリックに自分で定義したオブジェクト型を渡して、引数にStreamReaderSystem.Stringを渡すことで、文字列情報をオブジェクトにデシリアライズしてくれます。
    また、横道に逸れますがDeserializerBuilderを使ってデシリアライザをビルドすることで、Yamlファイルに出力される文字列の表記をキャメルケースやスネークケースに変えてくれたりするような細かい設定ができるようになります。

YamlDotNet.RepresentationModel

  • YamlStream
    メンバ関数のLoadの引数にTextReaderを渡すと、辞書型で各ノードの情報が格納されたYamlStream型が取得できます。
    メンバ変数のDocumentsには、Yamlの区切り文字---または終端文字...を認識して、複数のドキュメントとして配列にして返してくれます。Documents配列内の変数の型は、YamlNodeという基底クラスを継承したノード達が格納されています。厳密な部分は少し端折っていますが、このDocumentの中を型に注意しながら見ていけばいいんだなぁというのが伝われば幸いです。

  • YamlNode
    以下で紹介する各ノード種類の継承元クラス。以下がモデルの構成図です。
    image.png
    (引用元:https://yaml.org/spec/1.2/spec.html

  • YamlScalarNode
    子要素を持たないノード(葉ノード)です。アクセサであるValueを持っています。

  • YamlSequenceNode
    ノードが格納されている配列型です。子を持つ配列形式のノードはこの型になります。

  • YamlMappingNode
    ノードが格納されている辞書型です。子を持つハッシュ形式のノードはこの型になります。

今回書きたいyamlファイルを定義

Version:1.0.0
Date:{yyyy-MM-dd}
PersonInfo:
    HP:20
    MP:10
    Attack:4
    Defence:5
    Speed:2
    Luck:3
Classes:
    - Name:せんし
      Strategy:ガンガンいこうぜ
      Feature:力持ち
    - Name:そうりょ
      Strategy:いのちだいじに
      Feature:癒し系
    - Name:まほうつかい
      Strategy:じゅもんつかうな
      Feature:素手派
Items:
    - Heal:やくそう
      Doping:ちからのたね
      Weapon:ひのきのぼう
      TechniqueMachines:
        - Machine01:きあいパンチ
          Machine04:めいそう
          Machine08:ビルドアップ
          Machine13:れいとうビーム

とりあえずこのくらいに対応すれば一通り流れがつかめると思うのでこれで行きます。

それではやっていきましょう!

Yamlファイルを出力(シリアライズ)

まずは出力先のフォルダです。
私は今回、C\DQ\フォルダを作成し、そこに出力していきます。

自分の中の勝手な風習として先に読み込みたいんですけど、先にファイル用意したほうが手打ちより楽そうなのでこっちから行きます(もうQiitaのマークダウンで書いているが)。

それではオブジェクトを使うパターンとそうでないパターンの2種類のコードを書いていきます。

オブジェクトを使うやり方

まずは地道にオブジェクトを作ります。

public class YamlObj
{
    public string Version { get; set; }
    public DateTime Date { get; set; }
    public PersonInfo PersonInfo { get; set; }
    public Classes[] Classes { get; set; }
    public Items[] Items { get; set; }
}

public class PersonInfo
{
    public int HP { get; set; }
    public int MP { get; set; }
    public int Attack { get; set; }
    public int Defence { get; set; }
    public int Speed { get; set; }
    public int Luck { get; set; }
}

public class Classes
{
    public string Name { get; set; }
    public string Strategy { get; set; }
    public string Feature { get; set; }
}

public class Items
{
    public string Heal { get; set; }
    public string Doping { get; set; }
    public string Weapon { get; set; }
    public TechniqueMachines[] TechniqueMachines { get; set; }
}

public class TechniqueMachines
{
    public string Machine01 { get; set; }
    public string Machine04 { get; set; }
    public string Machine08 { get; set; }
    public string Machine13 { get; set; }
}

この時、変数名がYamlファイルのキーとして出力されてくるので、出力する分にはいいですが、読み込む際には十分に注意する必要があります。

それでは適当な関数を用意して、シリアライズしていきます。

var yamlObj = new YamlObj
{
    Version = "1.0.0",
    Date = new DateTime(1988, 2, 10),
    PersonInfo = new PersonInfo
    {
        HP = 20,
        MP = 10,
        Attack = 4,
        Defence = 5,
        Speed = 2,
        Luck = 3
    },
    Classes = new Classes[]
    {
        new Classes
        {
            Name = "せんし",
            Strategy = "ガンガンいこうぜ",
            Feature = "力持ち"
        },
        new Classes
        {
            Name = "そうりょ",
            Strategy = "いのちだいじに",
            Feature = "癒し系"
        },
        new Classes
        {
            Name = "まほうつかい",
            Strategy = "じゅもんつかうな",
            Feature = "素手派"
        },
    },
    Items = new Items[]
    {
        new Items
        {
            Heal  = "やくそう",
            Doping  = "ちからのたね",
            Weapon = "ひのきのぼう",
            TechniqueMachines = new TechniqueMachines[]
            {
                new TechniqueMachines
                {
                Machine01 = "きあいパンチ",
                Machine04 = "めいそう",
                Machine08 = "ビルドアップ",
                Machine13 = "れいとうビーム"
                }
            }
        }
    }
};

// シリアライズ
string savePath = @"C:\DQ\param.yaml";
using TextWriter writer = File.CreateText(savePath);
var serializer = new Serializer();    
serializer.Serialize(writer, YamlObj);

出力結果はこんな感じ。
image.png

ストリームを使うやり方

var techniqueMachines = new YamlMappingNode(
    new YamlScalarNode("Machine01"), new YamlScalarNode("きあいパンチ"),
    new YamlScalarNode("Machine04"), new YamlScalarNode("めいそう"),
    new YamlScalarNode("Machine08"), new YamlScalarNode("ビルドアップ"),
    new YamlScalarNode("Machine13"), new YamlScalarNode("れいとうビーム")
);
var stream = new YamlStream(
    new YamlDocument(
        new YamlMappingNode(
            new YamlScalarNode("Version"), new YamlScalarNode("1.0.0"),
            new YamlScalarNode("Date"), new YamlScalarNode($"{ new DateTime(1988, 2, 10) }"),
            new YamlScalarNode("PersonInfo"),
            new YamlMappingNode(
                new YamlScalarNode("HP"), new YamlScalarNode("20"),
                new YamlScalarNode("MP"), new YamlScalarNode("10"),
                new YamlScalarNode("Attack"), new YamlScalarNode("4"),
                new YamlScalarNode("Defence"), new YamlScalarNode("5"),
                new YamlScalarNode("Speed"), new YamlScalarNode("2"),
                new YamlScalarNode("Luck"), new YamlScalarNode("3")
            ),
            new YamlScalarNode("Classes"),
            new YamlSequenceNode(
                new YamlMappingNode(
                    new YamlScalarNode("Name"), new YamlScalarNode("せんし"),
                    new YamlScalarNode("Strategy"), new YamlScalarNode("ガンガンいこうぜ"),
                    new YamlScalarNode("Feature"), new YamlScalarNode("力持ち")
                ),
                new YamlMappingNode(
                    new YamlScalarNode("Name"), new YamlScalarNode("そうりょ"),
                    new YamlScalarNode("Strategy"), new YamlScalarNode("いのちだいじに"),
                    new YamlScalarNode("Feature"), new YamlScalarNode("癒し系")
                ), 
                    new YamlMappingNode(
                    new YamlScalarNode("Name"), new YamlScalarNode("まほうつかい"),
                    new YamlScalarNode("Strategy"), new YamlScalarNode("じゅもんつかうな"),
                    new YamlScalarNode("Feature"), new YamlScalarNode("素手派")
                )
            ),
            new YamlScalarNode("Items"),
            new YamlMappingNode(
                new YamlScalarNode("Heal"), new YamlScalarNode("やくそう"),
                new YamlScalarNode("Doping"), new YamlScalarNode("ちからのたね"),
                new YamlScalarNode("Weapon"), new YamlScalarNode("ひのきのぼう"),
                new YamlScalarNode("TechniqueMachines"),
                techniqueMachines
            )
        )
    )
);

// シリアライズ
string savePath = @"C:\DQ\param.yaml";
using TextWriter writer = File.CreateText(savePath));
stream.Save(writer, false);

どうせなのでもう1パターン。YamlMappingNodeのAdd関数を使っていく方法です。自分的にはこっちの方が好みです。

// 1行目を直接記述
var version = "---\nVersion: 1.0.0\n";
var sr = new StringReader(version);
var stream = new YamlStream();
stream.Load(sr);

// 先ほど書いた1行目を根ノードとして取得
var root_node = (YamlMappingNode)stream.Documents[0].RootNode;
root_node.Add("Date", $"{ new DateTime(1988, 2, 10) }");

// PersonInfo
var personInfo = new YamlMappingNode();
personInfo.Add("HP", "20");
personInfo.Add("MP", "10");
personInfo.Add("Attack", "4");
personInfo.Add("Defence", "5");
personInfo.Add("Speed", "2");
personInfo.Add("Luck", "3");
root_node.Add("PersonInfo", personInfo);

// Classes
var classes = new YamlSequenceNode();
var warrior = new YamlMappingNode();
warrior.Add("Name", "せんし");
warrior.Add("Strategy", "ガンガンいこうぜ");
warrior.Add("Feature", "力持ち");
var priest = new YamlMappingNode();
priest.Add("Name", "そうりょ");
priest.Add("Strategy", "いのちだいじに");
priest.Add("Feature", "癒し系");
var mage = new YamlMappingNode();
mage.Add("Name", "まほうつかい");
mage.Add("Strategy", "じゅもんつかうな");
mage.Add("Feature", "素手派");
classes.Add(warrior);
classes.Add(priest);
classes.Add(mage);
root_node.Add("Classes", classes);

// Items
var items = new YamlSequenceNode();
var item = new YamlMappingNode();
item.Add("Heal", "やくそう");
item.Add("Doping", "ちからのたね");
item.Add("Weapon", "ひのきのぼう");
var techniqueMachines = new YamlSequenceNode();
var techniqueMachine = new YamlMappingNode();
techniqueMachine.Add("Machine01", "きあいパンチ");
techniqueMachine.Add("Machine04", "めいそう");
techniqueMachine.Add("Machine08", "ビルドアップ");
techniqueMachine.Add("Machine13", "れいとうビーム");
techniqueMachines.Add(techniqueMachine);
item.Add("TechniqueMachines", techniqueMachines);
items.Add(item);
root_node.Add("Items", items);

// シリアライズ
string savePath = @"C:\DQ\param.yaml";
using TextWriter writer = File.CreateText(savePath);
stream.Save(writer, false);

出力結果はこんな感じ
image.png
YamlStream.SaveでシリアライズしたYamlファイルの末尾には自動で終端記号の...がつくみたいですね。
気になるようでしたらオブジェクトを使って出力すれば解決できそう←

Yamlファイル読み込み(デシリアライズ)

こちらもオブジェクトを使うパターンとそうでないパターンの2種類のコードを書いていきます。

オブジェクトを使うやり方

オブジェクトは先ほど定義したものを再利用して、デシリアライザを使ってオブジェクトを取得する関数を作成します。

public class YamlImporter
{
    public static YamlData Deserialize(string yamlPath)
    {
        // テキスト抽出
        var input = new StreamReader(yamlPath, Encoding.UTF8);

        // デシリアライザインスタンス作成
        var deserializer = new Deserializer();

        // yamlデータのオブジェクトを作成
        var deserializeObject = deserializer.Deserialize<YamlData>(input);

        return deserializeObject;
    }
}

ではこれで返ってくるオブジェクトを早速確認してみます。

// オブジェクト作成
string yamlPath = @"C:\DQ\param.yaml";
YamlObj yamlObj = YamlImporter.Deserialize(yamlPath);

Console.WriteLine(yamlObj.Version);
Console.WriteLine(yamlObj.Date);
Console.WriteLine($"{yamlObj.PersonInfo.HP}\t{yamlObj.PersonInfo.MP}\t{yamlObj.PersonInfo.Attack}\t" +
    $"{yamlObj.PersonInfo.Defence}\t{yamlObj.PersonInfo.Speed}\t{yamlObj.PersonInfo.Luck}");
foreach (var tClass in yamlObj.Classes)
{
    Console.WriteLine($"{tClass.Name}\t{tClass.Strategy}\t{tClass.Feature}");
}
foreach (var item in yamlObj.Items)
{
    Console.WriteLine($"{item.Heal}\t{item.Doping}\t{item.Weapon}");
    foreach (var TechniqueMachine in item.TechniqueMachines)
    {
        Console.WriteLine($"{TechniqueMachine.Machine01}\t{TechniqueMachine.Machine04}\t" +
            $"{TechniqueMachine.Machine08}\t{TechniqueMachine.Machine13}");
    }
}

/*
>> 1.0.0
>> 1988/02/10 0:00:00
>> 20      10      4       5       2       3
>> せんし  ガンガンいこうぜ        力持ち
>> そうりょ        いのちだいじに  癒し系
>> まほうつかい    じゅもんつかうな        素手派
>> やくそう        ちからのたね    ひのきのぼう
*/

ストリームを使うやり方

最後にわざわざオブジェクト作らないで省エネに行くコードを書きます。

string yamlPath = @"C:\DQ\param.yaml";
var input = new StreamReader(yamlPath, Encoding.UTF8);
var yaml = new YamlStream();
yaml.Load(input);

まずはストリームで読み込んだ後に、YamlStreamってところに渡したら勝手にドキュメントごとに分割して返してくれます。
ドキュメントごとというのは、Yamlファイルの区切り文字である---のことですね。今回は1つだけ(---は使っていない)なので、決め打ちで配列の0番を指定しちゃいます。また、仕様だと思いますが、終端文字の...を記述していても、Documentsには追加されてしまいます。

var rootNode = yaml.Documents[0].RootNode;

繰り返しとはなりますが、DocumentのgetterにはAllNodesRootNodeの2種類あります。今回はRootNodeからたどっていく方法で実装していきます。

var version = rootNode["Version"];
Console.WriteLine(version);
var date = rootNode["Date"];
Console.WriteLine(date);
/*
>> 1.0.0
>> 1988-02-10T00:00:00.0000000
*/
var personInfo = (YamlMappingNode)rootNode["PersonInfo"];
foreach (var c in personInfo.Children)
{
    Console.WriteLine($"{c.Key} : {c.Value}");
}
/*
>> HP : 20
>> MP : 10
>> Attack : 4
>> Defence : 5
>> Speed : 2
>> Luck : 3
*/
var classes = (YamlSequenceNode)rootNode["Classes"];
foreach (var c in classes.Children)
{
    Console.WriteLine($"{c["Name"]}, {c["Strategy"]}, {c["Feature"]}");
}
/*
>> せんし, ガンガンいこうぜ, 力持ち
>> そうりょ, いのちだいじに, 癒し系
>> まほうつかい, じゅもんつかうな, 素手派
*/
var items = (YamlSequenceNode)rootNode["Items"];
foreach (var c in items.Children)
{
    Console.WriteLine($"{c["Heal"]}, {c["Doping"]}, {c["Weapon"]}");
    foreach (var cc in (YamlSequenceNode)c["TechniqueMachines"])
    {
        Console.WriteLine($"{cc["Machine01"]}, {cc["Machine04"]}, {cc["Machine08"]}, {cc["Machine13"]}");
    }
}
/*
>> やくそう, ちからのたね, ひのきのぼう
>> きあいパンチ, めいそう, ビルドアップ, れいとうビーム
*/

RootNodeからたどっていくとこんな感じの書き方になるんですかね?
最後のforeach内でYamlSequenceNodeにキャストしているのは、YamlNodeGetEnumeratorが実装されていない(foreachが使えない)からです。一回子どもに行くとYamlNodeになってしまうのでキャストします。

また、Classes以降はKeyは表示してないんですが、やるとしたらこんな感じになります。

var classes = (YamlSequenceNode)rootNode["Classes"];
foreach (YamlMappingNode c in classes.Children)
{
    foreach (var cc in c.Children)
    {
        Console.WriteLine($"{cc.Key} : {cc.Value}");
    }
}
/*
Name : せんし
Strategy : ガンガンいこうぜ
Feature : 力持ち
Name : そうりょ
Strategy : いのちだいじに
Feature : 癒し系
Name : まほうつかい
Strategy : じゅもんつかうな
Feature : 素手派
*/

Classesは配列型で定義しているのでまずはYamlSequenceNodeにキャストしてから、その子要素が配列型じゃなくなるタイミングでKeyValuePairが継承されているYamlMappingNodeにキャストすることでKeyValueが取得できます。
ですのでこれらを踏まえると、YamlNodeのgetterに存在するNodeTypeをチェックして配列型の時のみYamlMappingNodeにキャストするみたいな方法をとればKeyの手打ち入力は完全に不要になるはずです。
これで得体のしれないYamlファイルを読み込めます!(そんな場面があるのかはさておき)

さいごに

どうですかYamlDotNet!すごい便利!ステマじゃないですよ?
日本語の資料が少ないのが自分としては本当にネックなので皆さんでこの閑散とした世界を活発にしていきましょう!

また、配列の要素が1つしかないのに配列で定義していたりと意味の分からないコードになっていますが、サンプルになればいいなと思い書いたのでusing 温かい目で見守ってください。

Githubにも今回のコードを全て関数にして置いておいたので、ぜひ使ってください。
https://github.com/miwazawa/YamlDotNetSample
少しでも実装の手助けになったらハピネスです。
最後までありがとうございました!

参考

10
15
4

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
10
15