LlamaIndex SimpleDirectoryReader — file type not recognized or metadata lost
<code>SimpleDirectoryReader</code> is the default file-system reader. It handles the common cases but has silent failure modes on unusual encodings, unrecognized extensions, and metadata that developers need to be explicit about.
Quick fix (TL;DR)
SimpleDirectoryReader auto-detects file types and delegates to per-format readers. Fix issues by (a) explicitly configuring file_extractor for non-standard file types, (b) handling encoding failures with encoding="utf-8" and error strategies, (c) using file_metadata callback to preserve custom metadata, and (d) using per-format readers (PyMuPDFReader, UnstructuredReader) for files that need more processing than the default.Real error messages you'll see
These are the exact strings returned by the LlamaIndex framework and its integrations when this error occurs. Copy-paste-searching any of them should land on this page.
ValueError: File type .parquet is not supported. Consider providing a custom file_extractor or use a supported file type.
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xa3 in position 1247: invalid start byte # Windows-1252 file being read as UTF-8
# Document loaded but doc.metadata is {} — no source path, no file name,
# no custom fields the downstream code expected
Reference
Default file types SimpleDirectoryReader handles
| Extension | Reader used | Notes |
|---|---|---|
.pdf | PyPDFReader / PDFReader | Text only; images lost |
.docx | DocxReader | Requires python-docx |
.pptx | PptxReader | Slide text |
.md | MarkdownReader | Preserves structure |
.txt | Basic file reader | Encoding-sensitive |
.html | HTMLTagReader | Text-only extraction |
.csv | CSVReader | Row-per-node option |
.json | Not native — needs custom extractor | |
.epub | EpubReader | Available in community |
.jpg, .png | ImageReader with multimodal LLM | Requires vision-capable LLM |
Root causes, ranked by frequency
Based on developer reports across LlamaIndex forums, GitHub issues, and Discord community during 2025–2026.
- 26%File type not in the default extractor map. .parquet, .yml, .rst, .xml — no default reader.
- 18%Non-UTF-8 encoding. Windows / older docs in cp1252, latin-1, or shift-jis.
- 14%PDF is image-only (scanned). Default PDF reader extracts empty text; needs OCR.
- 10%Custom metadata not attached. file_metadata callback missing; downstream code assumes fields exist.
- 8%docx / pptx / epub package not installed. Optional deps missing; reader silently skips file type.
- 7%File path with unicode characters. On some OS + Python combos, paths with non-ASCII chars fail to open.
- 10%Recursive read finds too many files. Massive directory tree; readers pulled into memory all at once.
- 7%.gitignore-style files included. Reader picks up hidden files, lock files, temp files.
Fixes — copy-paste solutions
Configure file_extractor for non-standard file types
Pass a file_extractor dict mapping extensions to reader instances. Any extension not in the map either falls back to default or raises, depending on required_exts.
from llama_index.core import SimpleDirectoryReader, Document from llama_index.readers.file import PyMuPDFReader from typing import List import json # Custom reader for .json files (not in defaults) from llama_index.core.readers.base import BaseReader class JSONReader(BaseReader): def load_data(self, file, extra_info=None) -> List[Document]: with open(file, "r", encoding="utf-8") as f: data = json.load(f) # Serialise back to text for embedding text = json.dumps(data, indent=2, ensure_ascii=False) return [Document(text=text, metadata={"source": str(file), **(extra_info or {})})] # Custom reader for .parquet class ParquetReader(BaseReader): def load_data(self, file, extra_info=None) -> List[Document]: import pyarrow.parquet as pq table = pq.read_table(file) df = table.to_pandas() # Emit one document per row (or aggregate — your choice) docs = [] for i, row in df.iterrows(): text = "\n".join(f"{col}: {val}" for col, val in row.items()) docs.append(Document(text=text, metadata={ "source": str(file), "row_index": i, **(extra_info or {}), })) return docs reader = SimpleDirectoryReader( input_dir="./data", recursive=True, file_extractor={ ".json": JSONReader(), ".parquet": ParquetReader(), ".pdf": PyMuPDFReader(), # override default with PyMuPDF for better extraction }, required_exts=[".pdf", ".docx", ".md", ".txt", ".json", ".parquet"], exclude_hidden=True, filename_as_id=True, ) documents = reader.load_data() print(f"Loaded {len(documents)} documents") for doc in documents[:3]: print(f" {doc.metadata.get('source', '?')} -> {len(doc.text)} chars")
PyMuPDFReader extracts better than the default in most cases and preserves layout metadata. For scanned PDFs (image-only), use UnstructuredReader or an OCR-based extractor.Handle encoding correctly for legacy text files
Pass encoding= explicitly to file readers. For unknown encoding, use chardet to detect, or use errors="replace" as a safety net.
from llama_index.core import SimpleDirectoryReader, Document from llama_index.core.readers.base import BaseReader import chardet class SafeTextReader(BaseReader): """Text reader that detects encoding and falls back gracefully.""" def load_data(self, file, extra_info=None): # First, detect encoding with open(file, "rb") as f: raw = f.read() detected = chardet.detect(raw) encoding = detected["encoding"] or "utf-8" confidence = detected["confidence"] try: text = raw.decode(encoding) except UnicodeDecodeError: # Fallback: replace un-decodable chars text = raw.decode(encoding or "utf-8", errors="replace") return [Document( text=text, metadata={ "source": str(file), "encoding": encoding, "encoding_confidence": confidence, **(extra_info or {}), }, )] reader = SimpleDirectoryReader( input_dir="./legacy_data", recursive=True, file_extractor={".txt": SafeTextReader(), ".log": SafeTextReader()}, ) docs = reader.load_data() # Audit encoding detection low_confidence = [d for d in docs if d.metadata.get("encoding_confidence", 1) < 0.9] if low_confidence: print(f"⚠ {len(low_confidence)} documents had low-confidence encoding detection") for d in low_confidence[:5]: print(f" {d.metadata['source']} -> {d.metadata['encoding']} " f"({d.metadata['encoding_confidence']:.0%})")
Preserve rich metadata with file_metadata callback
Pass a callable to file_metadata. It receives each file path and returns a dict merged into every document from that file. Perfect for adding department, category, freshness, or source-type tags.
from pathlib import Path from datetime import datetime from llama_index.core import SimpleDirectoryReader def file_metadata_fn(file_path: str) -> dict: """Attach metadata based on file location and stats.""" p = Path(file_path) stat = p.stat() # Derive category from directory structure parts = p.parts category = parts[-3] if len(parts) >= 3 else "uncategorized" return { "file_name": p.name, "file_path": str(p.resolve()), "file_type": p.suffix.lstrip("."), "file_size_bytes": stat.st_size, "created_at": datetime.fromtimestamp(stat.st_ctime).isoformat(), "modified_at": datetime.fromtimestamp(stat.st_mtime).isoformat(), "category": category, # Add your own domain-specific tags "department": "engineering" if "eng" in str(p).lower() else "other", } reader = SimpleDirectoryReader( input_dir="./docs", recursive=True, file_metadata=file_metadata_fn, # ← attaches to every doc exclude_hidden=True, exclude=[".git", "node_modules", "__pycache__", "*.tmp"], required_exts=[".md", ".pdf", ".txt"], ) docs = reader.load_data() # Metadata flows through the whole pipeline — # available on retrieved nodes, so downstream can filter/cite by category etc. for doc in docs[:3]: print(doc.metadata) # On queries, metadata is preserved on the nodes for filter push-down: from llama_index.core.vector_stores import MetadataFilter, MetadataFilters filters = MetadataFilters(filters=[ MetadataFilter(key="category", value="policies"), MetadataFilter(key="department", value="engineering"), ]) # Use filters on the query engine (see page 109)
Prevention checklist
Ship these seven safeguards once and this error stops appearing in your logs.
- Explicitly configure
file_extractorfor every file type you expect. - Use
required_extsto whitelist supported extensions; fail loud on unexpected ones. - For non-UTF-8 sources, detect encoding at read time and store the encoding in metadata.
- Attach source-aware metadata with
file_metadata=callback— enables filter push-down later. - For PDFs, prefer
PyMuPDFReaderorUnstructuredReaderover the default. - Use
exclude=to skip.git, lock files, and temp files. - Audit the loaded document count against the expected file count — silent skips are the biggest ingestion risk.
Frequently asked questions
S3Reader, GCSReader, or AzureBlobStorageReader from llama-index-readers-* packages. They provide the same interface but authenticate against cloud storage.UnstructuredReader with strategy="hi_res" (requires tesseract) or a dedicated OCR reader. For LlamaIndex-native OCR, wrap ImageReader with a vision-capable LLM.Document is the input unit — one file or logical document. Nodes are the split pieces used for embedding and retrieval. The node parsers turn documents into nodes.iter_data() instead of load_data() — returns an iterator. Useful for very large corpora where holding everything in memory is impractical.Document can have identical metadata but distinct doc_id. For explicit doc-id control, use filename_as_id=True or set doc.id_ = "custom-id" after loading.Related errors & hubs
Get the weekly AI-error digest
New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.