Fix Invalid UTF-8 Errors in JSON

JSON text must be Unicode, and PHP’s JSON functions expect strings to be valid UTF-8. The error “Malformed UTF-8 characters, possibly incorrectly encoded” usually starts earlier when bytes were decoded with the wrong source encoding.

Last updated: September 26, 2026.

function toUtf8(string $value, string $sourceEncoding): string
{
    if ($sourceEncoding === 'UTF-8') {
        if (!mb_check_encoding($value, 'UTF-8')) {
            throw new UnexpectedValueException('Invalid UTF-8 input');
        }
        return $value;
    }

    return mb_convert_encoding($value, 'UTF-8', $sourceEncoding);
}

$payload['name'] = toUtf8($payload['name'], 'Windows-1252');
$json = json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);

The source encoding is an explicit input, not a guess. JSON_THROW_ON_ERROR prevents a silent false result and identifies the failing operation.

Find the bad field

Validate strings at the boundary where files, database drivers, or external APIs enter the application. Recursively inspect a payload with mb_check_encoding and log the field path—not the sensitive field value. If the database is involved, verify the connection character set as well as the column definition.

Choose conversion or rejection

Convert when the upstream encoding is known, such as a documented Windows-1252 export. Reject input when the source promises UTF-8 but sends invalid bytes; that makes the producer fix the contract. JSON_INVALID_UTF8_SUBSTITUTE can replace invalid sequences with U+FFFD, but substitution loses information and should be an explicit product decision.

The PHP json_encode documentation lists the UTF-8 flags and versions. Do not use JSON_INVALID_UTF8_IGNORE by default because discarded bytes can change names, identifiers, or meaning.

Separate syntax from encoding

A string can be valid UTF-8 but invalid JSON because of commas, quotes, or nesting. Conversely, structurally correct JSON can contain invalid input bytes. Test decoding with JSON_THROW_ON_ERROR; use json_validate() only when you need validation without building the decoded structure. For large inputs, process a stream as described in the large JSON guide.

Related Web Cheat Sheet guides

Sergey Kornilov

Sergey Kornilov