基礎から学ぶC#入門 90日コース | アプリを作りながら学ぶ - Day 78:JSON保存

Web APP 90日で身につけるC#
スポンサーリンク
スポンサーリンク

Day 78 のゴールと全体像

Day 78 では、タスク管理アプリに 「記憶力」を持たせていきます。 これまでは、アプリを終了すると、せっかく登録したタスクがすべて消えてしまう状態でした。 今日は、JSONファイルにタスクを保存し、次回起動時に読み込むことで、

アプリを閉じてもタスクが残り続ける

という、実用的な振る舞いを手に入れていきます。

「データを保存する」というのは、 アプリ開発の世界ではとても重要なテーマです。 その第一歩として、JSON保存をやさしく体験していきましょう。

JSONってそもそも何者?

人間にも読みやすいデータの形

JSON(ジェイソン)は、

  • テキストで書かれた
  • 「キーと値」の組み合わせを
  • シンプルなルールで表現する

ためのフォーマットです。

例えば、タスクを JSON で表すと、こんな感じになります。

{
  "Id": 1,
  "Title": "レポート作成",
  "Description": "明日までに提出",
  "Priority": 1,
  "DueDate": "2026-09-10T00:00:00",
  "IsCompleted": false,
  "CreatedAt": "2026-09-06T17:30:00"
}
JSON

見てみると、

  • Id
  • Title
  • Description

など、C# の TaskItem クラスのプロパティ名がそのまま出てきています。 この「クラスの形とほぼ同じ形で保存できる」というのが、JSONの大きな魅力です。

どこに保存するかを決める

ファイルパスを決める

コンソールアプリでは、 まず「どのファイルに保存するか」を決める必要があります。

今回は、アプリと同じフォルダに tasks.json というファイルを作ることにしましょう。

// JSONファイルのパス(今回はカレントディレクトリに保存)
const string TaskFilePath = "tasks.json";
C#

この定数を Program クラスの中に置いておくと、 アプリ全体で同じファイルを参照できて便利です。

JSON保存に使うライブラリを準備する

System.Text.Json を使う

C# では、標準ライブラリとして System.Text.Json が用意されています。 これを使うことで、

  • オブジェクト → JSON文字列(シリアライズ)
  • JSON文字列 → オブジェクト(デシリアライズ)

を簡単に行うことができます。

必要な using は次のとおりです。

using System.Text.Json;
using System.IO;
C#

System.IO はファイルの読み書きに使います。

TaskRepository に「保存」と「読み込み」を追加する

タスク一覧を丸ごと保存する

まずは、TaskRepository に「今持っているタスク一覧を JSON に保存する」メソッドを追加します。

class TaskRepository
{
    private readonly List<TaskItem> _tasks = new List<TaskItem>();
    private int _nextId = 1;

    // 既存のメソッドは省略...

    // タスク一覧をJSONファイルに保存する
    public void SaveToFile(string filePath)
    {
        // 1. シリアライズ用のオプションを設定(見やすいようにインデントを付ける)
        var options = new JsonSerializerOptions
        {
            WriteIndented = true // JSONを整形して保存(人間にも読みやすく)
        };

        // 2. タスク一覧をJSON文字列に変換
        string json = JsonSerializer.Serialize(_tasks, options);

        // 3. ファイルに書き込む
        File.WriteAllText(filePath, json);
    }

    // JSONファイルからタスク一覧を読み込む
    public void LoadFromFile(string filePath)
    {
        // ファイルが存在しない場合は、何もせずに戻る
        if (!File.Exists(filePath))
        {
            return;
        }

        // 1. ファイルからJSON文字列を読み込む
        string json = File.ReadAllText(filePath);

        // 2. JSON文字列をList<TaskItem>に変換
        var tasks = JsonSerializer.Deserialize<List<TaskItem>>(json);

        if (tasks == null)
        {
            // 変換に失敗した場合は何もしない(簡易的なエラーハンドリング)
            return;
        }

        // 3. 内部のリストを読み込んだデータで置き換える
        _tasks.Clear();
        _tasks.AddRange(tasks);

        // 4. 次に割り当てるIDを調整する
        if (_tasks.Any())
        {
            _nextId = _tasks.Max(t => t.Id) + 1;
        }
        else
        {
            _nextId = 1;
        }
    }
}
C#

重要ポイントをじっくり解説

  • WriteIndented = true JSONを「インデント付き」で保存することで、 ファイルを開いたときに人間にも読みやすくなります。 デバッグや学習のときには、とてもありがたい設定です。
  • SerializeDeserialize
    • Serialize:オブジェクト → JSON文字列
    • Deserialize:JSON文字列 → オブジェクト という役割を持っています。 List<TaskItem> をそのまま渡せるのが嬉しいところです。
  • ファイルがない場合の扱い 初回起動時など、まだ tasks.json が存在しないこともあります。 その場合は、何もせずに戻ることで、 「空の状態からスタートする」挙動になります。
  • _nextId の調整 読み込んだタスクの最大IDを調べて、 その次の番号を _nextId に設定しています。 これをしないと、再起動後にIDが1から再スタートしてしまい、 既存タスクとIDがかぶってしまうので注意が必要です。

アプリ起動時にJSONを読み込む

Mainの最初で読み込む

アプリを起動したときに、 前回保存したタスクを読み込むようにします。

class Program
{
    static TaskRepository repository = new TaskRepository();
    const string TaskFilePath = "tasks.json";

    static void Main()
    {
        // アプリ起動時にJSONファイルからタスクを読み込む
        repository.LoadFromFile(TaskFilePath);

        Run();

        // アプリ終了時にタスクを保存する
        repository.SaveToFile(TaskFilePath);
    }

    // Runやその他のメソッドはこれまで通り...
}
C#

起動時読み込み・終了時保存の流れ

  • 起動時 LoadFromFile を呼び出して、 tasks.json があれば中身を読み込み、 なければ空の状態でスタートします。
  • 終了時 Run() が終わったタイミングで SaveToFile を呼び出し、 現在のタスク一覧を tasks.json に書き出します。

この2つを入れるだけで、

アプリを閉じてもタスクが残る

という、かなり「本物感」のある挙動になります。

例:JSON保存付きタスク管理アプリの全体イメージ

すべてを書くと長くなるので、 JSON保存に関係する部分を中心にしたイメージを示します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.IO;

class TaskItem
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public int Priority { get; set; }
    public DateTime? DueDate { get; set; }
    public bool IsCompleted { get; set; }
    public DateTime CreatedAt { get; set; }
}

class TaskRepository
{
    private readonly List<TaskItem> _tasks = new List<TaskItem>();
    private int _nextId = 1;

    public TaskItem Add(string title, string description, int priority, DateTime? dueDate)
    {
        var task = new TaskItem
        {
            Id = _nextId++,
            Title = title,
            Description = description,
            Priority = priority,
            DueDate = dueDate,
            IsCompleted = false,
            CreatedAt = DateTime.Now
        };

        _tasks.Add(task);
        return task;
    }

    public IEnumerable<TaskItem> GetAll()
    {
        return _tasks;
    }

    public TaskItem FindById(int id)
    {
        return _tasks.FirstOrDefault(t => t.Id == id);
    }

    public bool Remove(int id)
    {
        var task = FindById(id);
        if (task == null)
        {
            return false;
        }

        _tasks.Remove(task);
        return true;
    }

    // JSONファイルに保存
    public void SaveToFile(string filePath)
    {
        var options = new JsonSerializerOptions
        {
            WriteIndented = true
        };

        string json = JsonSerializer.Serialize(_tasks, options);

        File.WriteAllText(filePath, json);
    }

    // JSONファイルから読み込み
    public void LoadFromFile(string filePath)
    {
        if (!File.Exists(filePath))
        {
            // ファイルがない場合は何もしない
            return;
        }

        string json = File.ReadAllText(filePath);

        var tasks = JsonSerializer.Deserialize<List<TaskItem>>(json);

        if (tasks == null)
        {
            return;
        }

        _tasks.Clear();
        _tasks.AddRange(tasks);

        if (_tasks.Any())
        {
            _nextId = _tasks.Max(t => t.Id) + 1;
        }
        else
        {
            _nextId = 1;
        }
    }
}

class Program
{
    static TaskRepository repository = new TaskRepository();
    const string TaskFilePath = "tasks.json";

    static void Main()
    {
        // 起動時に読み込み
        repository.LoadFromFile(TaskFilePath);

        Run();

        // 終了時に保存
        repository.SaveToFile(TaskFilePath);
    }

    static void Run()
    {
        while (true)
        {
            Console.WriteLine("=== タスク管理アプリ ===");
            Console.WriteLine("1: タスク追加");
            Console.WriteLine("2: タスク一覧");
            Console.WriteLine("0: 終了");
            Console.Write("選択してください: ");

            string input = Console.ReadLine();

            switch (input)
            {
                case "1":
                    CreateTask();
                    break;
                case "2":
                    ShowTasks();
                    break;
                case "0":
                    Console.WriteLine("終了します。");
                    return;
                default:
                    Console.WriteLine("不正な入力です。");
                    break;
            }

            Console.WriteLine();
        }
    }

    static void CreateTask()
    {
        Console.WriteLine("=== タスク追加 ===");

        Console.Write("タスク名を入力してください: ");
        string title = Console.ReadLine();

        Console.Write("詳細(メモ)を入力してください(空でも構いません): ");
        string description = Console.ReadLine();

        Console.Write("優先度を入力してください (1: 高, 2: 中, 3: 低): ");
        int priority = int.Parse(Console.ReadLine() ?? "2");

        Console.Write("期限を入力してください (yyyy-MM-dd、未入力なら期限なし): ");
        string dueText = Console.ReadLine();
        DateTime? dueDate = null;
        if (!string.IsNullOrWhiteSpace(dueText) && DateTime.TryParse(dueText, out DateTime d))
        {
            dueDate = d;
        }

        var task = repository.Add(title, description ?? string.Empty, priority, dueDate);

        Console.WriteLine($"タスクを追加しました。ID: {task.Id}");
    }

    static void ShowTasks()
    {
        Console.WriteLine("=== タスク一覧 ===");

        var tasks = repository.GetAll().ToList();

        if (!tasks.Any())
        {
            Console.WriteLine("タスクが登録されていません。");
            return;
        }

        foreach (var t in tasks)
        {
            string status = t.IsCompleted ? "[完了]" : "[未完了]";
            string priorityText = t.Priority switch
            {
                1 => "高",
                2 => "中",
                3 => "低",
                _ => "不明"
            };
            string dueText = t.DueDate.HasValue
                ? t.DueDate.Value.ToString("yyyy-MM-dd")
                : "期限なし";

            Console.WriteLine($"ID: {t.Id}");
            Console.WriteLine($"  状態   : {status}");
            Console.WriteLine($"  優先度 : {priorityText}");
            Console.WriteLine($"  期限   : {dueText}");
            Console.WriteLine($"  タイトル: {t.Title}");
            Console.WriteLine($"  詳細   : {t.Description}");
            Console.WriteLine();
        }
    }
}
C#

このサンプルは最小限の機能だけですが、 JSON保存の流れを体験するには十分な形になっています。

セキュリティ・安全性の視点を少しだけ

天才プログラマー兼セキュリティスペシャリストっぽく、 少しだけ「安全性」の話もしておきます。

  • ファイルパスの扱い 実際のアプリでは、ユーザーごとに保存場所を変えたり、 OSごとの適切なフォルダ(ドキュメントフォルダなど)を使うこともあります。
  • JSONの中身の検証 本格的なアプリでは、 読み込んだJSONが壊れていないか、 想定外の値が入っていないかをチェックすることも大切です。
  • 機密情報の保存 パスワードや個人情報など、 機密性の高い情報をJSONに平文で保存するのは危険です。 そういった場合は暗号化や専用のストレージを検討する必要があります。

今回のタスク管理アプリは学習用なので、 そこまで厳しく考える必要はありませんが、 「データを保存する」という行為には、 こうしたセキュリティの視点もついて回るんだな、 と頭の片隅に置いておいていただけると、 今後の成長にきっと役立ちます。

Day 78 のまとめ

Day 78 では、タスク管理アプリに JSON保存機能を追加しました。

  • JSONとは何かをざっくり理解し
  • System.Text.Json を使って
    • タスク一覧をJSON文字列にシリアライズし
    • JSONファイルからタスク一覧をデシリアライズし
  • アプリ起動時に読み込み、終了時に保存する流れを作り
  • _nextId を調整して、IDの一貫性を保つようにしました。

これで、タスク管理アプリは、

「その場限りの練習用」から 「継続して使える自分専用ツール」

へと、一段階レベルアップしたと言ってよい状態になりました。

ぜひ、いくつかタスクを登録してからアプリを終了し、 もう一度起動してみてください。 tasks.json を開いて中身を眺めてみるのも、とても良い学びになります。

自分が書いたコードが、 ちゃんと「記憶」を持って動いている―― その感覚は、きっと少し誇らしくて、 そして次のステップに進みたくなる、 そんなワクワクを連れてきてくれるはずです。

タイトルとURLをコピーしました