Skip to content

Parquet reader: option to replace invalid UTF-8 instead of failing #12047

Open
antonireus/arrow-rs
#1

Description

@antonireus

Is your feature request related to a problem or challenge?

The Parquet reader fails the whole read when a STRING column holds bytes that are not valid UTF-8:

Parquet error: encountered non UTF-8 data: invalid utf-8 sequence of 1 bytes from index 80822

Files like this are unfortunately common in practice, for example Latin-1 text written as STRING by older producers. Other readers handle them:

  • Arrow C++ (and so pyarrow and R) and arrow-go don't fail. They put the raw bytes into the string array and leave the UTF-8 check to later steps (ValidateFull). Users can still get at the data and clean it up.
  • The JVM side doesn't fail either. parquet-java and iceberg-java turn the bytes into strings with U+FFFD for invalid sequences, and Spark reads them without error.
  • arrow-rs fails, and there is no workaround:

This blocks the Rust engines that need to read what the JVM ecosystem reads. DataFusion Comet is stuck on exactly this: its native scan fails where Spark succeeds (apache/datafusion-comet#4121, tracked in apache/datafusion-comet#4764).

Describe the solution you'd like

A reader option that picks what happens on invalid UTF-8:

pub enum InvalidUtf8 {
    /// Fail the read (current behavior, default)
    Error,
    /// Replace invalid sequences with U+FFFD per value, as String::from_utf8_lossy does
    Replace,
}

ArrowReaderOptions::with_invalid_utf8(InvalidUtf8)
  • Output is always valid UTF-8
  • New behaviour is opt-in, so by default nothing changes
  • The replacement is consistent with what the std Rust library already does in String::from_utf8_lossy
  • Results match the Java/Spark ecosystem, which already replaces, and this unblocks Comet.
  • No cost on valid data. Validation stays as it is. Only the existing error branch changes.

Describe alternatives you've considered

Additional context

No response

Activity

  1. andygrove commented on Nov 4, 2026

    @andygrove
    Member

    +1 to this. DataFusion Comet would use Replace, and it would let us close several issues tracked in apache/datafusion-comet#4764.

    Two points on the design.

    The check should key on the target type, not only the annotation. The byte array decoders validate UTF-8 only when the column is annotated UTF8. A BINARY column without the annotation that is read as Utf8, LargeUtf8, Utf8View, or a dictionary of those, through ArrowReaderOptions::with_schema, is never checked, so it produces an invalid string array today (#12063). With the check keyed on the annotation alone, Replace would not apply there, and that is the other half of what Comet needs: Spark reads such columns as strings with spark.sql.parquet.binaryAsString. DataFusion also hits this through its binary-to-string schema coercion (apache/datafusion#27065).

    I had opened #12064 to fix that case on its own (validate whenever the target is a string type, and error on invalid UTF-8). I have closed it in favor of folding it into this option, since Error/Replace is the better place to decide what happens. The branch may still be useful: it threads the flag through the byte array, byte view and dictionary decoders, fixes the dictionary decoder's value type for string dictionaries over unannotated columns (they returned Binary values, or panicked for Utf8View), and has a test over the six string types with plain and dictionary encoding.

    Replacement semantics. String::from_utf8_lossy follows the Unicode "maximal subpart" practice. The JVM's UTF-8 decoder, which Spark uses, emits a different number of U+FFFD for some ill-formed sequences, encoded surrogates (ED A0..BF ..) among them. For Comet it would help if Replace matched the JVM, or if the replacement were pluggable. If that is out of scope, matching from_utf8_lossy is still a big improvement, and we can document the difference.

    Are you planning to implement this? If not, I'd be happy to put up a PR building on the #12064 branch.

  2. alamb commented on Nov 4, 2026

    @alamb
    Contributor

    Stripping the annotation reads the bytes unchecked, which is #11140.

    This seems like the right behavior to me. If the file is invalid utf8, we shouldn't be able to put it in a StringArray that requires utf8 for safetly

    The error doesn't name the column.

    It would be nice to make a small PR to improve the error message

  3. alamb commented on Nov 4, 2026

    @alamb
    Contributor

    I suggest we

    1. fix / do fix(parquet): validate UTF-8 when reading an unannotated BINARY column as a string #12064
    2. Then we can separately evaluate the policy mode of automatically fixing up invalid data during read

    It seems to me they are both worthwhile and independent

  4. antonireus commented on Nov 4, 2026

    @antonireus
    Author

    @andygrove

    Are you planning to implement this? If not, I'd be happy to put up a PR building on the #12064 branch.
    I could try, but I'm not very familiar with the code base, and I would need to rely too much on Claude criteria 😅

    @alamb

    Stripping the annotation reads the bytes unchecked, which is #11140.

    This seems like the right behavior to me. If the file is invalid utf8, we shouldn't be able to put it in a StringArray that requires utf8 for safetly

    I agree, I mentioned it only to make aware that stripping the annotation is a dead end, as #11140 should eventually be fixed

  5. hassaanch23 commented on Nov 6, 2026

    @hassaanch23

    For step 1, #11144 is that standalone fix, for #11140. It has been open since September, and I rebased it onto main last week. It takes the same approach as #12064:

    • The array readers turn on set_validate_utf8 based on the Arrow type they build: Utf8, LargeUtf8, Utf8View, or a dictionary of one of those.
    • The dictionary decoder takes its value type from whether it validates. String dictionaries over unannotated columns, Dictionary<_, Utf8View> included, now get string values.

    The tests read valid and invalid data into each string type, over five kinds of pages: dictionary, a dictionary that falls back to PLAIN, PLAIN, DELTA_LENGTH_BYTE_ARRAY and DELTA_BYTE_ARRAY. They also cover a JSON column, which is read as Utf8 but was never validated. @kita-renji tested it against 165 cases, and @Rich-T-kid is reviewing it now.

    If that works for everyone, it could land as step 1, and the InvalidUtf8 option could then build on the same flag. cc @andygrove, since #12064 makes the same change.

  6. alamb commented on Nov 8, 2026

    @alamb
    Contributor

    For step 1, #11144 is that standalone fix, f

    Thanks -- I left some comments (I am hoping we can simplify that PR before merge, but it is a good start)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions