A data import should distinguish valid text from bytes that only become readable after replacement characters are inserted. When byte validity matters, create a UTF-8 decoder with an explicit error policy and let malformed input fail before parsing the resulting text.

The JDK 21 CharsetDecoder documentation describes decoding and its error-handling methods. CodingErrorAction.REPORT requests that errors be reported. The example uses the convenience decode method for a complete byte array, so callers receive a CharacterCodingException on invalid input.

Prerequisites

Use JDK 21 with both javac and java available. Save this program as StrictUtf8.java. No dependency manager or external library is needed. The input bytes are synthetic fixtures created inside the program.

import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;

public class StrictUtf8 {
    static String decode(byte[] bytes) throws CharacterCodingException {
        return StandardCharsets.UTF_8.newDecoder()
            .onMalformedInput(CodingErrorAction.REPORT)
            .onUnmappableCharacter(CodingErrorAction.REPORT)
            .decode(ByteBuffer.wrap(bytes)).toString();
    }
    public static void main(String[] args) throws Exception {
        String text = "tea \u2615";
        if (!decode(text.getBytes(StandardCharsets.UTF_8)).equals(text))
            throw new AssertionError("round trip");
        if (!decode(new byte[0]).isEmpty()) throw new AssertionError("empty");
        byte[][] bad = {
            {(byte) 0xC3, 0x28},
            {(byte) 0xE2, (byte) 0x82},
            {(byte) 0xC0, (byte) 0xAF}
        };
        int rejected = 0;
        for (byte[] bytes : bad) {
            try { decode(bytes); }
            catch (CharacterCodingException expected) { rejected++; }
        }
        if (rejected != bad.length) throw new AssertionError("accepted bad UTF-8");
        System.out.println("valid UTF-8 round trip passed");
        System.out.println("malformed sequences rejected: " + rejected);
    }
}

Compile, run, and compare

javac StrictUtf8.java
java StrictUtf8
valid UTF-8 round trip passed
malformed sequences rejected: 3

Understand the fixtures

The positive case contains ordinary letters and a non-ASCII coffee symbol. Encoding and decoding must reproduce the same Java string. An empty array is valid and decodes to an empty string. The negative cases exercise an invalid continuation, a truncated multibyte sequence, and an overlong representation. All three must throw.

The checks use explicit AssertionError statements rather than the Java assert keyword, so they remain active without the -ea runtime flag. The main method counts rejected fixtures and fails if any malformed sequence is accepted. A successful process exit therefore means more than merely printing an example string.

Use it at an import boundary

Place strict decoding before a CSV parser, configuration loader, or other text consumer when the source contract requires UTF-8. Handle the exception as an import error and report the affected record or file through your application's normal diagnostics. Preserve the original bytes when investigation is required; a replacement-decoded string cannot reconstruct them.

Encoding validity is only one layer. Valid UTF-8 can still contain unwanted control characters, unexpected normalization forms, or content that violates the application schema. Those checks belong after decoding. This program also does not remove a byte-order mark or normalize text.

Troubleshooting and boundaries

If a supposedly valid file fails, confirm its declared encoding instead of switching to a permissive fallback. This function expects the entire byte sequence. Do not call it independently on arbitrary network chunks because a multibyte character may cross a chunk boundary. Large streaming inputs require the decoder's incremental lifecycle and retained incomplete bytes; that path is outside the tested example.

Find more examples in the Java category.