# File Handling

Course: [Dart](https://www.devtutorials.ai/learn/dart) · Section: File Handling
Source: https://www.devtutorials.ai/learn/dart/files-and-json/file-handling

> How to read and write files and folders with dart:io, and how to save and load data as JSON with dart:convert, model classes and safe parsing.

## What Is File Handling?

**File handling** = reading data **from** files and writing data **to** files on a disk.

- Variables live only while the program runs. When it ends, they are gone.
- Files **stay** after the program ends. Next time, you can read them again.
- In Dart, file tools come from the library **`dart:io`**.

**Simple lesson:** files are the program's long-term memory.

### A Real-World Example

Think of the class **attendance register**.

- Every morning, the teacher **reads** yesterday's page.
- She **adds** today's names at the bottom. She does not tear out old pages.
- At the end of the year, a new register is **created**, and old ones are kept in a **cupboard** (a folder).
- If the register is missing, she reports it, instead of giving up teaching.

Try each action on a pretend file:

Every **Run** box on this page has its own **pretend disk**, made fresh for each run. It already holds:

- `notes.txt`, `marks.csv` and a read-only `locked.txt`
- a folder `data` with `students.json` and `config.json`

On your computer, `dart:io` uses your real disk. `dart:io` does **not** work in web apps.

## What You Will Learn

In this lesson, you will learn:

1. Paths, and how to read and write files, sync and async.
2. How to append, check file details and work with folders.
3. How to handle missing files and permissions.
4. How to encode and decode JSON.
5. How to write model classes with `fromJson` and `toJson`, and parse safely.

## Paths

**Path** = the address of a file on the disk.

| Kind | Starts from | Example |
|---|---|---|
| **Absolute** path | the top of the disk | `/home/student/notes.txt` or `C:\school\notes.txt` |
| **Relative** path | the **current folder** of the program | `notes.txt`, `data/students.json` |

- `..` means "the folder above".
- Prefer `/` in Dart code. It works on Windows too.

```dart runnable title="A relative path and its absolute form"
import 'dart:io';

void main() {
  var f = File('data/students.json');
  print(f.path);
  print(f.absolute.path);
  print(Directory.current.path);
}
```

**Simple lesson:** a relative path starts from where the program runs, and an absolute path starts from the top of the disk.

## Reading Files

### Whole File, Lines, or Bytes

| Method | Gives |
|---|---|
| `readAsStringSync()` | the whole file as one `String` |
| `readAsLinesSync()` | a `List<String>`, one item per line |
| `readAsBytesSync()` | a `List<int>` of raw bytes |

```dart runnable title="Reading the notes file"
import 'dart:io';

void main() {
  var notes = File('notes.txt');
  var text = notes.readAsStringSync();
  print(text.length);

  var lines = notes.readAsLinesSync();
  for (var i = 0; i < lines.length; i++) {
    print('${i + 1}. ${lines[i]}');
  }
}
```

### Reading a CSV File

**CSV** = "comma-separated values": a table written as text, one row per line.

```dart runnable title="Average marks from a CSV file"
import 'dart:io';

void main() {
  var rows = File('marks.csv').readAsLinesSync();
  print('Header: ${rows.first}');
  for (var row in rows.skip(1)) {
    var parts = row.split(',');
    var maths = int.parse(parts[1]);
    var science = int.parse(parts[2]);
    print('${parts[0]}: ${((maths + science) / 2).toStringAsFixed(1)}');
  }
}
```

**Simple lesson:** read the whole text, the lines, or the bytes, whichever suits your file.

## Sync and Async

Every file method has **two** versions:

| Sync | Async | Difference |
|---|---|---|
| `readAsStringSync()` | `readAsString()` | async returns a `Future<String>` |
| `writeAsStringSync(t)` | `writeAsString(t)` | async returns a `Future<File>` |
| `existsSync()` | `exists()` | async returns a `Future<bool>` |

- **Sync** waits right there. Simple, fine for small scripts.
- **Async** lets the program do other work while the disk is busy. Use it in apps and servers. (See **Async Basics and Futures**.)

```dart runnable title="The async way with await"
import 'dart:io';

Future<void> main() async {
  var file = File('notes.txt');
  if (await file.exists()) {
    var lines = await file.readAsLines();
    print('${lines.length} notes found');
    print('First: ${lines.first}');
  }
}
```

**Simple lesson:** `...Sync()` waits for the disk, and the version without `Sync` returns a Future.

## Writing Files

### Write and Replace

**`writeAsStringSync(text)`** creates the file if it is missing, and **replaces** everything if it exists.

### Append

**Append** = add to the **end** of a file, keeping what was there. Pass `mode: FileMode.append`.

```dart runnable title="Saving a small report"
import 'dart:io';

void main() {
  var report = File('report.txt');
  report.writeAsStringSync('Class 7A report\n');
  var marks = {'Asha': 92, 'Ravi': 75, 'Priya': 85};
  for (var e in marks.entries) {
    report.writeAsStringSync('${e.key}: ${e.value}\n', mode: FileMode.append);
  }
  print(report.readAsStringSync());
}
```

Without `mode: FileMode.append`, every write **wipes** the file first.

Many beginners lose their saved data this way. Use append when you are adding records.

**Simple lesson:** a normal write replaces the file, and `FileMode.append` adds to its end.

## File Details

| Method | Gives |
|---|---|
| `existsSync()` | `true` if the file is there |
| `lengthSync()` | size in bytes |
| `lastModifiedSync()` | a `DateTime` of the last change |
| `deleteSync()` | removes the file |
| `renameSync('new.txt')` | moves or renames it |

```dart runnable title="Checking, measuring and deleting"
import 'dart:io';

void main() {
  var f = File('old_notice.txt');
  f.writeAsStringSync('Holiday on Friday');
  print(f.existsSync());
  print(f.lengthSync());
  f.deleteSync();
  print(f.existsSync());
}
```

**Simple lesson:** check `exists` before you read, and use `length` and `lastModified` to learn about a file.

## Directories

**Directory** = a folder. Use the `Directory` class.

```dart runnable title="Making a folder and listing it"
import 'dart:io';

void main() {
  var reports = Directory('reports/2026');
  reports.createSync(recursive: true);
  File('reports/2026/7A.txt').writeAsStringSync('Class 7A');
  File('reports/2026/7B.txt').writeAsStringSync('Class 7B');

  for (var item in reports.listSync()) {
    print(item.path);
  }
  print(Directory('data').listSync().length);
}
```

- **`recursive: true`** also makes the missing parent folders (`reports`).
- **`listSync()`** gives the files and folders inside. `list()` is the async version, a Stream.
- `deleteSync(recursive: true)` removes a folder with everything in it. Be careful.

**Simple lesson:** `Directory` makes, lists and deletes folders, just like `File` does for files.

## When Things Go Wrong

File work fails more than most code: the file is missing, or you are not allowed to touch it.

- **`FileSystemException`** = the parent type for file problems.
- **`PathNotFoundException`** = a kind of `FileSystemException`: the path does not exist.

```dart runnable title="Handling a missing file and a locked file"
import 'dart:io';

void main() {
  try {
    File('homework.txt').readAsStringSync();
  } on PathNotFoundException {
    print('homework.txt is missing. Starting a new one.');
  }

  try {
    File('locked.txt').writeAsStringSync('Changed!');
  } on FileSystemException catch (e) {
    print('Could not write: ${e.message}');
  }
}
```

**Easy way to remember**

- `import 'dart:io';` first.
- **read**: `readAsString`, `readAsLines`, `readAsBytes`.
- **write**: `writeAsString` replaces, `mode: FileMode.append` adds.
- **check**: `exists`, `length`, `lastModified`, `delete`.
- Wrap file work in `try` with `on FileSystemException`.

**Simple lesson:** always expect file errors, and catch `FileSystemException` so the program can recover.

## What Is JSON?

**JSON** (JavaScript Object Notation) = a simple **text** format for data. Almost every app and server uses it to send data.

- **Object** = `{ "key": value, ... }`. In Dart it becomes a `Map<String, dynamic>`.
- **Array** = `[value, value, ...]`. In Dart it becomes a `List<dynamic>`.
- Values can be strings (in **double** quotes), numbers, `true`, `false`, `null`, objects and arrays.

**Simple lesson:** JSON is data written as text, so any program can read it.

### A Real-World Example

The school app shows your marks.

- The marks live on the school's **server**.
- The server sends them to your phone as **JSON text**: `{"name": "Asha", "marks": [92, 88]}`.
- The app **decodes** the text into Dart values, and shows them on screen.
- When you update your profile, the app **encodes** your data as JSON and sends it back.

Edit the JSON text and see what Dart gets:

`jsonEncode` and `jsonDecode` come from `dart:convert`. Remember the import.

## JSON Structure

| JSON | Dart | Example |
|---|---|---|
| object `{ }` | `Map<String, dynamic>` | `{"name": "Asha"}` |
| array `[ ]` | `List<dynamic>` | `[92, 88]` |
| string | `String` | `"Blue"` |
| number | `int` or `double` | `13`, `1.45` |
| `true` / `false` | `bool` | `true` |
| `null` | `null` | `null` |

JSON is strict:

- Keys **must** be in double quotes: `"name"`, not `name` or `'name'`.
- No comma after the last item.
- No comments.

**Simple lesson:** JSON has objects, arrays and simple values, and Dart turns them into maps, lists and values.

## Encoding and Decoding

### jsonEncode

**Encode** = Dart value → JSON text.

```dart runnable title="A map becomes JSON text"
import 'dart:convert';

void main() {
  var student = {'name': 'Asha', 'age': 13, 'monitor': true, 'marks': [92, 88]};
  var text = jsonEncode(student);
  print(text);
  print(text.length);
}
```

For text that people read, use `JsonEncoder.withIndent`:

```dart runnable title="Pretty JSON"
import 'dart:convert';

void main() {
  var config = {'school': 'Green Valley', 'year': 2026, 'houses': ['Red', 'Blue']};
  print(JsonEncoder.withIndent('  ').convert(config));
}
```

### jsonDecode

**Decode** = JSON text → Dart values.

**Simple lesson:** `jsonEncode` writes Dart data as text, and `jsonDecode` reads text back into Dart data.

## Maps, Lists and Nested JSON

`jsonDecode` returns **`dynamic`**. Tell Dart what you expect, so the rest of the code is typed:

```dart runnable title="Nested JSON from a server"
import 'dart:convert';

void main() {
  var text = '''
  {
    "class": "7A",
    "teacher": {"name": "Mrs. Iyer", "subject": "Science"},
    "students": [
      {"name": "Asha", "marks": 92},
      {"name": "Ravi", "marks": 75}
    ]
  }
  ''';
  var data = jsonDecode(text) as Map<String, dynamic>;
  var teacher = data['teacher'] as Map<String, dynamic>;
  var students = data['students'] as List<dynamic>;

  print('${data['class']} is taught by ${teacher['name']}');
  for (var s in students) {
    print('${s['name']}: ${s['marks']}');
  }
}
```

- An object **inside** an object: read it as another map.
- An array of objects: a list whose items are maps.

**Simple lesson:** follow the shape of the JSON: maps inside maps, and lists of maps.

## Model Classes

Reading `s['name']` everywhere is easy to get wrong. A **model class** gives the data real fields.

- **`fromJson`** = a factory constructor that builds an object **from** a map.
- **`toJson`** = a method that turns the object **into** a map.
- `jsonEncode` automatically calls `toJson()` on objects it does not know.

```dart runnable title="Student.fromJson and toJson"
import 'dart:convert';

class Student {
  final String name;
  final int age;
  final List<int> marks;

  Student(this.name, this.age, this.marks);

  factory Student.fromJson(Map<String, dynamic> json) {
    return Student(
      json['name'] as String,
      json['age'] as int,
      (json['marks'] as List).cast<int>(),
    );
  }

  Map<String, dynamic> toJson() => {'name': name, 'age': age, 'marks': marks};

  int get total => marks.fold(0, (a, b) => a + b);
}

void main() {
  var s = Student.fromJson(jsonDecode('{"name": "Asha", "age": 13, "marks": [92, 88]}'));
  print('${s.name} scored ${s.total}');
  print(jsonEncode(s));
}
```

### Lists of Objects and Nested Objects

```dart runnable title="A class with its students"
import 'dart:convert';

class Student {
  final String name;
  final int marks;
  Student(this.name, this.marks);

  factory Student.fromJson(Map<String, dynamic> j) => Student(j['name'], j['marks']);
  Map<String, dynamic> toJson() => {'name': name, 'marks': marks};
}

class Section {
  final String code;
  final List<Student> students;
  Section(this.code, this.students);

  factory Section.fromJson(Map<String, dynamic> j) => Section(
        j['code'],
        (j['students'] as List).map((s) => Student.fromJson(s)).toList(),
      );

  Map<String, dynamic> toJson() => {'code': code, 'students': students};
}

void main() {
  var text = '{"code": "7B", "students": [{"name": "Priya", "marks": 85}, {"name": "Imran", "marks": 79}]}';
  var section = Section.fromJson(jsonDecode(text));
  print('${section.code} has ${section.students.length} students');
  print(section.students.last.name);
  print(jsonEncode(section));
}
```

`jsonEncode(section)` calls `section.toJson()`, which holds `Student` objects, so it calls their `toJson()` too.

`jsonEncode` fails on objects with no `toJson()`, and on things like `DateTime` and `Set`.

Convert them first: a date to `date.toIso8601String()`, and a set to `set.toList()`.

**Simple lesson:** `fromJson` turns a map into an object, and `toJson` turns it back, so the rest of your code uses real fields.

## Parsing Safely

Data from outside can be **wrong**: a key may be missing, a value may be `null`, or a number may be text.

### Bad JSON Text

```dart runnable title="Catching broken JSON"
import 'dart:convert';

void main() {
  for (var text in ['{"name": "Asha"}', "{name: 'Asha'}"]) {
    try {
      var data = jsonDecode(text);
      print('OK: ${data['name']}');
    } on FormatException {
      print('Not valid JSON: $text');
    }
  }
}
```

### Missing Keys and null

Use the tools from **Null Safety**: `?`, `??` and type checks:

```dart runnable title="Defaults for missing data"
import 'dart:convert';

void main() {
  var data = jsonDecode('{"name": "Ravi", "house": null}') as Map<String, dynamic>;
  var name = data['name'] as String? ?? 'Unknown';
  var house = data['house'] as String? ?? 'Not chosen yet';
  var age = data['age'] as int? ?? 0;
  print('$name, $house, age $age');
}
```

### Validating with Patterns

A pattern (from **Pattern Matching**) checks keys **and** types in one step:

```dart runnable title="Accepting only well-formed students"
import 'dart:convert';

void main() {
  var replies = [
    '{"name": "Asha", "marks": 92}',
    '{"name": "Ravi", "marks": "ninety"}',
    '{"marks": 70}',
  ];
  for (var r in replies) {
    var data = jsonDecode(r);
    if (data case {'name': String name, 'marks': int marks}) {
      print('Saved $name with $marks');
    } else {
      print('Rejected: $r');
    }
  }
}
```

### Where JSON Is Used

| Use | Example |
|---|---|
| API responses | the server sends a student list |
| Config files | `config.json` with the school name and settings |
| Local storage | saving a user's choices to a file |

**Easy way to remember**

- `import 'dart:convert';`
- **encode** = Dart → text. **decode** = text → Dart.
- object → `Map<String, dynamic>`, array → `List<dynamic>`.
- **fromJson** in, **toJson** out.
- Never trust outside data: catch `FormatException`, use `??`, and check with patterns.

**Simple lesson:** treat outside JSON as untrusted, and check it before you use it.

## TL;DR

1. **`dart:io`** gives `File` and `Directory`. Not available on the web.
2. **Read:** `readAsString`, `readAsLines`. **Write:** `writeAsString`, `FileMode.append`. Each has a Sync version.
3. Handle **`FileSystemException`** for missing or locked files.
4. **JSON** (`dart:convert`): `jsonEncode` → text, `jsonDecode` → maps and lists.
5. **Model classes:** `fromJson` and `toJson`. Parse safely with `??`, type checks and patterns.

## Common mistakes

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Forgetting `import 'dart:io';` | `File` is built in | `File` comes from `dart:io` | `Undefined name 'File'`. |
| Writing without append | Writes add to the file | A normal write replaces it | Old data is lost. |
| Reading without checking | The file is always there | Check `exists` or catch the error | `PathNotFoundException`. |
| Mixing `readAsString()` with no `await` | It gives the text | It gives a `Future<String>` | You print `Instance of 'Future<String>'`. |
| `createSync()` on `a/b/c` | Parent folders appear | Pass `recursive: true` | A `PathNotFoundException`. |
| Using `dart:io` in a web app | It works everywhere | Browsers have no file system access like this | The import fails. |
| Forgetting `import 'dart:convert';` | JSON tools are built in | Import `dart:convert` | `Method not found: 'jsonEncode'`. |
| Single quotes in JSON text | JSON is like Dart | JSON needs double quotes | `FormatException`. |
| Reading `data.name` | Decoded JSON has fields | It is a map: `data['name']` | A compile error, or `NoSuchMethodError`. |
| Encoding an object with no `toJson()` | Dart knows how | Write `toJson()` | `Converting object to an encodable object failed`. |
| Trusting every key | The server is always right | Use `??` and type checks | A crash on missing data. |
| `json['marks'] as List<int>` | The cast converts | Use `(json['marks'] as List).cast<int>()` | A type error. |

## Check Yourself

## Exercises

## What to Remember

- Files keep data after the program ends.
- A normal write replaces a file, and `FileMode.append` adds to it.
- Expect file errors, and catch `FileSystemException`.
- JSON is text data, and `jsonEncode` and `jsonDecode` convert it.
- Model classes with `fromJson` and `toJson` keep JSON code clean.

Next: **Dart Cheat Sheet**. All the syntax from this course on one page.
