Question

Read an InputStream into a String in Kotlin

kotlininputstream

Question

Given an InputStream in Kotlin, what is the idiomatic, concise, and safe way to read its entire contents into a String?

The solution should:

  • close the stream reliably,
  • use an explicit character encoding,
  • avoid unnecessarily verbose line-by-line loops, and
  • preserve the original text, including line separators, when appropriate.

For example, instead of manually copying bytes or reading and appending each line, how should this be written idiomatically?

Short Answer

You will learn to convert a Kotlin InputStream to a String with bufferedReader(), readText(), and use. You will also learn why choosing a charset and avoiding line-by-line concatenation matters.

Concept

An InputStream supplies bytes, not text. To turn those bytes into a String, Kotlin and Java must decode them using a character set such as UTF-8.

The usual idiomatic Kotlin form is:

val text = input.bufferedReader(Charsets.UTF_8).use { reader ->
    reader.readText()
}

This works because:

  • bufferedReader(Charsets.UTF_8) wraps the byte stream in a BufferedReader that decodes UTF-8 bytes into characters efficiently.
  • readText() reads all remaining characters from the reader.
  • use { ... } closes the reader when the block finishes, even if an exception occurs.
  • Closing the reader also closes the underlying InputStream.

An explicit charset is important. Without one, code may use the machine's default charset, which can differ between computers, operating systems, containers, and production servers. UTF-8 is the common choice when a format or API specifies UTF-8.

Mental Model

Think of an InputStream as a pipe delivering numbered beads: bytes. A String is readable text.

A charset is the decoding legend that says how to interpret each bead or group of beads as letters. UTF-8 is one such legend. If you use the wrong legend, text such as café, emoji, or non-English characters can be decoded incorrectly.

use is like borrowing a book from a library with an automatic return box: after the block ends, Kotlin closes the resource for you, including when something goes wrong.

Take Quiz

Syntax and Examples

Use bufferedReader with the expected charset, then call readText() inside use:

import java.io.InputStream

fun InputStream.readAllUtf8(): String =
    bufferedReader(Charsets.UTF_8).use { reader ->
        reader.readText()
    }

Usage:

val input: InputStream = javaClass.getResourceAsStream("/message.txt")!!
val message = input.readAllUtf8()

println(message)

For a one-off read, an extension function is not required:

val text = input.bufferedReader(Charsets.UTF_8).use { it.readText() }

readText() reads the complete remaining content. Unlike readLine(), it does not split text into lines and therefore preserves existing newline characters.

If you already have a Reader, do not create another reader. Call readText() directly:

val text = reader.use { it.readText() }

Step by Step Execution

Consider this input, encoded as UTF-8:

Hello
Kotlin

Code:

val text = input.bufferedReader(Charsets.UTF_8).use { reader ->
    reader.readText()
}

Execution:

  1. input provides raw bytes.
  2. bufferedReader(Charsets.UTF_8) creates a BufferedReader that converts those UTF-8 bytes into characters and buffers reads for efficiency.
  3. use starts a block with that reader.
  4. reader.readText() reads every remaining character, producing the string "Hello\nKotlin\n" when the source ends with a newline.
  5. The value returned by the block becomes text.
  6. use closes the reader.
  7. Closing the reader closes input as well.

The resulting string retains the line breaks because no code removed or replaced them.

Real World Use Cases

This pattern is useful whenever a resource is known to contain a reasonably sized text document:

  • Reading a JSON error response from an HTTP connection.
  • Loading a text file packaged in application resources.
  • Reading a SQL script, configuration file, template, or Markdown document.
  • Reading a command-line process's text output.
  • Parsing a small CSV document before passing its text to another parser.

For example, reading a UTF-8 JSON response body from a HttpURLConnection:

val body = connection.inputStream
    .bufferedReader(Charsets.UTF_8)
    .use { it.readText() }

Only read the entire stream at once when its possible size is bounded or reasonably small. Large files and unbounded network responses should usually be processed incrementally.

Take Quiz

Real Codebase Usage

In real projects, developers often wrap this operation in a small extension function when the same encoding convention is used repeatedly:

import java.io.InputStream

fun InputStream.readUtf8Text(): String =
    bufferedReader(Charsets.UTF_8).use { it.readText() }

For an HTTP response, it is common to select the appropriate stream and then close it immediately after consuming it:

val stream = if (connection.responseCode in 200..299) {
    connection.inputStream
} else {
    connection.errorStream
}

val responseText = stream?.bufferedReader(Charsets.UTF_8)?.use { it.readText() }

For large data, process data as it arrives instead of building one giant string. For example, process lines only when line boundaries are meaningful:

input.bufferedReader(Charsets.UTF_8).useLines { lines ->
    lines.filter { it.isNotBlank() }.forEach { line ->
        println(line)
    }
}

Use useLines for line-oriented processing, not as the default way to reconstruct the original text.

Common Mistakes

Forgetting to close the stream

This leaves a file handle, socket, or other resource open:

val text = input.bufferedReader(Charsets.UTF_8).readText()

Use use when your code owns the stream:

val text = input.bufferedReader(Charsets.UTF_8).use { it.readText() }

Relying on the platform default charset

val text = input.bufferedReader().use { it.readText() }

This may be acceptable only when the input's encoding is intentionally the platform default. Prefer the encoding defined by the file format, HTTP header, or API contract:

val text = input.bufferedReader(Charsets.UTF_8).use { it.readText() }

Reading lines and accidentally removing newlines

readLine() returns a line without its line separator. This code changes "one\ntwo\n" into "onetwo":

val text = input.bufferedReader().use { reader ->
    buildString {
        while (true) {
            val line = reader.readLine() ?: break
            append(line)
        }
    }
}

Use readText() when preserving the text matters. If you intentionally need normalized lines, join them explicitly:

val text = input.bufferedReader(Charsets.UTF_8).use { reader ->
    reader.lineSequence().joinToString("\n")
}

This intentionally normalizes line separators and may not restore a final newline.

Using lineSequence() without closing the reader

val text = input.bufferedReader().lineSequence().joinToString("\n")

The sequence does not automatically close the reader. Put the full sequence consumption inside use.

Loading an unbounded or huge stream

readText() creates one complete String in memory. Do not use it for arbitrarily large uploads, logs, or responses. Stream and process chunks or lines instead.

Closing a stream you do not own

use closes the resource. If an API gives you an InputStream that another part of the program must continue using, do not close it. In that situation, document ownership clearly and read only the required portion.

Comparisons

ApproachBest useCloses resource?Preserves original line separators?
bufferedReader(UTF_8).use { it.readText() }Read an entire text streamYesYes
useLines { ... }Process lines one at a timeYesNo; separators are not included in lines
forEachLine { ... }Perform an action for each lineYesNo; separators are not included in lines
readBytes() then decodeBinary-first workflowsOnly with surrounding useYes after correct decoding
Manual readLine() loopCustom line handlingOnly if manually closed or wrapped in useNo unless you add separators yourself

readText() is the clearest choice when the goal is exactly: “read all text from this stream.”

useLines and forEachLine are preferable when the file may be large and you can handle one line at a time.

A byte-based alternative can be useful when you first need bytes for another reason:

val text = input.use { it.readBytes().toString(Charsets.UTF_8) }

For ordinary text input, bufferedReader(...).use { it.readText() } communicates intent more directly.

Take Quiz

Cheat Sheet

// Recommended: read all UTF-8 text and close the stream
val text = input.bufferedReader(Charsets.UTF_8).use { it.readText() }

// Reusable extension
fun InputStream.readUtf8Text(): String =
    bufferedReader(Charsets.UTF_8).use { it.readText() }

// Process, rather than store, lines
input.bufferedReader(Charsets.UTF_8).useLines { lines ->
    lines.forEach { line -> println(line) }
}
  • InputStream contains bytes; String contains decoded characters.
  • Specify the charset required by the input format; UTF-8 is common.
  • use closes resources even when an exception is thrown.
  • Closing a reader closes its wrapped InputStream.
  • readText() preserves line separators.
  • readLine(), lineSequence(), useLines, and forEachLine expose lines without their separators.
  • Avoid readText() for unbounded or very large input.

FAQ

What is the shortest idiomatic way to read an InputStream as a String in Kotlin?

val text = input.bufferedReader(Charsets.UTF_8).use { it.readText() }

It decodes UTF-8 text and closes the stream.

Does use close the underlying InputStream?

Yes. When use closes the BufferedReader, the reader closes the InputStream it wraps.

Should I always use UTF-8?

Use the charset specified by the source. UTF-8 is appropriate when the source is defined as UTF-8, which is common for JSON, modern APIs, and many text files.

Does readText() preserve newlines?

Yes. It reads characters as they occur in the stream. Line-based APIs such as readLine() remove line separators.

Why not use ByteArrayOutputStream().toString()?

It can work, but it is more verbose for text. Also, toString() without a charset can depend on the platform default charset. A reader makes text decoding explicit.

Is useLines better than readText()?

Use useLines when you want to process lines incrementally. Use readText() when you need one complete string and want to preserve original line separators.

Can I call readText() on a large file?

You can, but it stores all text in memory. Process large or untrusted-size streams incrementally instead.

What happens if reading fails inside use?

The exception is propagated, and use still attempts to close the resource.

Take Quiz

Mini Project

Description

Create a small loader that reads a UTF-8 application configuration from an InputStream. The loader should return the complete text, safely close the stream, and preserve the file's original line breaks.

Goal

Write a reusable Kotlin function that reads all UTF-8 text from an InputStream and demonstrate it with an in-memory configuration stream.

Requirements

Define an InputStream extension function named readUtf8Text. Decode the stream using Charsets.UTF_8. Close the reader and underlying stream automatically. Return all remaining text as one String. Demonstrate the function with a configuration string containing multiple lines.

Take Quiz

Keep learning

Related questions

View more