Files, I/O and JSON

java.nio.file Path and Files, buffered reading and writing, charsets, and serialising records to JSON safely.

Path and Files

Path dir = Path.of("data");
Path file = dir.resolve("orders.csv").normalize();

Files.exists(file);
Files.createDirectories(dir);
Files.size(file);
Files.isRegularFile(file);

// never resolve untrusted input without checking the result stays inside dir
Path candidate = dir.resolve(userName).normalize();
if (!candidate.startsWith(dir.toAbsolutePath().normalize())) {
    throw new IllegalArgumentException("path traversal attempt");
}
TaskCallNote
Join pathsdir.resolve("a")Never concatenate strings with a separator
Read all textFiles.readString(path)Fine up to a few megabytes
Write textFiles.writeString(path, text)Creates or truncates the file
Read linesFiles.readAllLines(path)Whole file in memory
Stream linesFiles.lines(path)Lazy — must be closed
Copy or moveFiles.copy, Files.movePass a copy option to replace
List a directoryFiles.list(dir)Returns a stream — must be closed
Walk a treeFiles.walk(dir)Must be closed; can be deep
MetadataFiles.readAttributes(path, ...)Fetches many attributes in one call
  • Path is an abstraction over separators, so resolve and normalize produce the right result on every platform.
  • A path is not a file: nothing is checked until an operation runs, so Files.exists is inherently racy and should be a hint, not a guarantee.
  • Handle IOException at the layer that can decide what to do — retry, use a default, or fail the request.
  • Prefer the Files helpers over File; the older class reports failures as a boolean instead of an exception, which loses the reason.

Reading and writing safely

try (var reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
    String line;
    while ((line = reader.readLine()) != null) {
        process(line);
    }
}

try (var writer = Files.newBufferedWriter(out, StandardCharsets.UTF_8)) {
    for (Order order : orders) {
        writer.write(order.toCsv());
        writer.newLine();
    }
}

// raw output streams: buffer them, or every small write is a syscall
try (var out = new BufferedOutputStream(Files.newOutputStream(target))) {
    out.write(payload);
}
  • Always pass a charset. The no-argument constructors use the platform default, so the same file decodes differently on a different machine.
  • try-with-resources closes in reverse order and closes even when the body throws, which is what keeps file handles from leaking.
  • Files.lines, Files.list and Files.walk return streams that hold an open directory handle — an unclosed one shows up later as a mysterious failure to delete or rename.
  • readString is convenient and loads everything; for a log file or an export, read line by line instead.
  • Writing to a temporary file and moving it into place makes an update atomic, so a crash cannot leave a half-written file.
💡
Buffering is the single highest-value change in most I/O code. A byte-at-a-time write to an unbuffered stream performs one system call per byte; wrapping it in a buffer turns thousands of calls into a handful.

JSON with records

public record Customer(String id, String email, List<String> tags) {}

ObjectMapper mapper = new ObjectMapper()
    .registerModule(new JavaTimeModule())
    .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
    .enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

String json = mapper.writeValueAsString(customer);        // records serialise directly
Customer back = mapper.readValue(json, Customer.class);
  • Serialise a dedicated DTO record rather than a database entity: the wire format then changes on your terms, not when someone renames a column.
  • Jackson binds records through their canonical constructor, so a missing required field fails with a clear error instead of leaving a null behind.
  • Decide the unknown-field policy explicitly and set it once on the shared mapper; the default has changed between versions and silently differs between libraries.
  • Never deserialise untrusted JSON into arbitrary types through polymorphic typing — that feature has a long history of being used as a gadget for remote code execution.
  • For a very large array, use the streaming API or read it in pages; mapping a hundred million elements into a List is a memory problem, not a JSON problem.
  • Keep one configured mapper and share it. Creating a new one per call is both slow and a source of inconsistent behaviour.

FAQ

Which JSON library should I pick?
Jackson for breadth and streaming, Gson for a small dependency, and the newer JSON-B or kotlinx.serialization in stacks that already standardise on them. What matters far more is having exactly one configured mapper in the application.
Why did my file write fail on Windows but work on Linux?
Windows locks files that are still open, and the platform separator and default charset differ. Resolve paths with Files helpers, always specify UTF-8, and close every reader and stream with try-with-resources.

Dates, text and numbers Modules, packages and build configuration

Last refreshed 2026-09-18.