Migrating to Tika 4.x
- Requirements
- Core SPI signatures:
InputStream→TikaInputStream tika-appandtika-serverdistributions: jar → zip- Default content handler: XHTML/XML → Markdown
- Configuration: XML to JSON
- Automatic Conversion
- Example Conversion
- Key Differences
- Parser Configuration Changes
- OCR engines and the
image/ocr-*pseudo-types (4.1.0+) - Engines under
parsersdeprecated (4.1.0+) - Embedding filters and the image embedding parser deprecated (4.1.0+)
- PDF page settings become the
pagesblock (4.1.0+) - Full Configuration Example
- Metadata Key Changes
- API Changes
- tika-grpc: generated Java classes moved package
- Timeout Model Changes
- Deprecations and Removals
This guide covers the changes required when upgrading from Apache Tika 3.x to 4.x.
Requirements
Java 17 or later (3.x required Java 11). See the Roadmap for version timelines and support schedules.
Core SPI signatures: InputStream → TikaInputStream
Parser, Detector and EmbeddedDocumentExtractor all changed shape. Every third-party
implementation of these interfaces must be updated; there is no InputStream overload, so the
break is a compile error rather than a silent behavior change.
// 3.x
void parse(InputStream stream, ContentHandler handler, Metadata metadata, ParseContext context);
MediaType detect(InputStream input, Metadata metadata);
// 4.x
void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata, ParseContext context);
MediaType detect(TikaInputStream tis, Metadata metadata, ParseContext parseContext);
EmbeddedDocumentExtractor.shouldParseEmbedded and parseEmbedded likewise take a
ParseContext, and parseEmbedded takes a TikaInputStream.
Callers wrap with TikaInputStream.get(…):
try (TikaInputStream tis = TikaInputStream.get(myInputStream)) {
parser.parse(tis, handler, metadata, context);
}
The Tika facade (Tika#parse, Tika#parseToString, Tika#detect) still accepts a plain
InputStream. One behavior change: Tika#detect(InputStream, …) no longer returns the
caller’s stream at its original position. The detector still resets the TikaInputStream it
reads — but that read-ahead is buffered inside an internal wrapper that detect() discards, so
the caller’s own stream comes back advanced. Pass a TikaInputStream you own (and rewind it),
or re-open the source. The stream is still not closed for you.
Why: passing TikaInputStream explicitly makes the spooling and rewind contract visible in the
signature instead of leaving every implementation to wrap defensively. See
spooling for the stream contract.
TikaInputStream itself dropped several members in 4.0: isTikaInputStream, cast,
getPath(int), get(InputStreamFactory) (both overloads), hasInputStreamFactory,
getInputStreamFactory, and setOpenContainer(Object, long) — along with the
InputStreamFactory class.
Two further TikaInputStream changes are easy to mistake for 3.x deltas but are deltas
against 4.0.0-beta-1: get(byte[]) and getFromContainer no longer declaring IOException
(neither declared it in 3.3.2), and enableRewind() throwing a checked IOException (the method
did not exist in 3.3.2). Upgrading from 3.x, neither is a change you need to make.
|
tika-app and tika-server distributions: jar → zip
In 3.x, tika-app-<version>.jar was a self-contained fat jar — you could drop it anywhere and run java -jar tika-app.jar. In 4.x it is a thin launcher that depends on the parsers, the Tika Pipes processor, and other modules living in an adjacent lib/ directory. Running the bare jar by itself will fail with NoClassDefFoundError.
Download tika-app-<version>.zip and run from inside the unzipped directory so lib/ (and plugins/) sit alongside the jar. The zip has no top-level directory, so unzip it into one:
unzip -d tika-app-<version> tika-app-<version>.zip
cd tika-app-<version>
java -jar tika-app-<version>.jar [option...] [file...]
tika-server-standard changed the same way, and it is the more common trap because the jar is
still published to Maven Central: tika-server-standard-<version>.jar is now a thin launcher
whose manifest Class-Path points at lib/. Pull it from Central on its own and it fails at
startup.
unzip -d tika-server-standard-<version> tika-server-standard-<version>.zip
cd tika-server-standard-<version>
java -jar tika-server-standard-<version>.jar
If you have build scripts or container images that drop in just the jar, update them to unpack the zip and run from inside it.
Default content handler: XHTML/XML → Markdown
In 3.x the default content handler produced XHTML/XML. In 4.x the default is Markdown everywhere:
-
tika-appoutputs Markdown by default (was XHTML). Pass-x/--xml,-h/--html, or-t/--textto choose another format. -
tika-server— the/tikaand/rmetaendpoints return Markdown content by default. In 3.x,/rmetareturned XML content, and a bare/tikaPUT routed among plain text, HTML, and XHTML byAcceptheader — nondeterministically for/. Use an explicit handler path (/tika/xml,/rmeta/xml, …) to choose another format. -
The async/pipes CLI emits Markdown by default (was plain text). Use
--handler x(etc.) to choose another format.
If you parse the extracted content programmatically and expect XHTML/XML, request it explicitly as shown above (TIKA-4663).
Configuration: XML to JSON
Tika 4.x uses JSON configuration files instead of XML. The legacy tika-config.xml format
is no longer supported.
Automatic Conversion
Tika provides a conversion tool in tika-app to help migrate your XML configuration:
java -jar tika-app.jar --convert-config-xml-to-json=tika-config.xml > tika-config.json
The converted JSON goes to standard output; there is no separate --config argument.
The converter currently supports:
-
Parsers section - parser declarations with parameters and exclusions
-
Parameter types - bool, int, long, double, float, string, list, and map
-
Special handling - TesseractOCR’s
otherTesseractSettingslist is automatically converted to theotherTesseractConfigmap format
|
The converter is a starting point, not a complete translation:
Review the generated JSON and confirm it loads before relying on it. |
Example Conversion
XML Format (3.x):
<properties>
<parsers>
<parser class="org.apache.tika.parser.pdf.PDFParser">
<params>
<param name="sortByPosition" type="bool">true</param>
<param name="maxMainMemoryBytes" type="long">1000000</param>
</params>
</parser>
<parser class="org.apache.tika.parser.DefaultParser">
<parser-exclude class="org.apache.tika.parser.pdf.PDFParser"/>
</parser>
</parsers>
</properties>
JSON Format (4.x):
{
"parsers": [
{
"pdf-parser": {
"sortByPosition": true,
"maxMainMemoryBytes": 1000000
}
},
{
"default-parser": {}
}
]
}
A parsers list loads only the parsers it names. The default-parser entry above
restores all the other parsers (it is the JSON equivalent of the 3.x DefaultParser).
Configuring a parser automatically excludes its default copy, so there is no duplication;
explicit exclude directives are only needed to disable a parser without replacing it.
|
Key Differences
| Aspect | XML (3.x) | JSON (4.x) |
|---|---|---|
Class references |
Full class name ( |
Kebab-case component name ( |
Parameters |
|
Direct key-value pairs |
Exclusions |
|
|
Parser Configuration Changes
The configuration options for PDFParser and TesseractOCRParser have changed
significantly in 4.x. The automatic converter will migrate your parameter names, but you
should review the updated documentation to ensure your configuration is optimal.
|
See the Configuration section for full details, including:
OCR engines and the image/ocr-* pseudo-types (4.1.0+)
Through 4.0.x an OCR engine was found because it advertised image/ocr-png, image/ocr-jpeg,
… in the parser registry. Since 4.1.0 an engine is found because it implements
org.apache.tika.parser.enricher.TextRecognizer and advertises the real types (image/png).
What changes for you:
-
A
_mime-include/_mime-excludenamingimage/ocr-*fails config load. To keep an engine off a type, filter the real type on the engine’s own entry ({"tesseract-ocr-parser": {"_mime-exclude": ["image/tiff"]}}); on thedefault-parserentry a real-type filter also removes the parser for that type. -
An engine the classpath supplied is never dispatched to;
image-parseralways invokes it, and the PDF parser’stextpolicy controls pages. Atk:content-type-parser-overrideofimage/ocr-*matches no parser any more and the document falls to the empty fallback parser: drop the override. -
Content-Typenever carries anocr-prefix any more, so extracted image files get their real extension instead of.bin. -
A custom engine that still advertises the pseudo-types keeps working as a text recognizer for the real types, with a WARN at startup, until 5.0. Advertise the real types and implement
TextRecognizerto silence it. -
Put the engine under
enginesand name it in the top-leveltext-recognizerslist rather than underparsers; every default parser stays loaded."text-recognizers": []turns enrichment off everywhere. An engine left underparserskeeps working in 4.1.x with a deprecation WARN at startup that says what the entry does (see below). Startup also logs one line per effective engine. -
VLM parsers gain
textRecognizer(defaulttrue); set it tofalsefor a captioning or tagging prompt so the PDF parser’sAUTOOCR never replaces extracted text with the VLM’s output.
Engines under parsers deprecated (4.1.0+)
An inference engine named as a parsers entry, an OCR engine, a VLM parser or the image
embedding parser, is deprecated since 4.1.0 and fails config load in 4.2.0, when parsers
holds parsers only. Startup logs one WARN per such entry saying what it does today: it parses
the types no other parser claims, it acts only as the text recognizer, or it never runs.
Configure the engine under engines and name it in text-recognizers (or bind it with
inference); every default parser stays loaded. Sending a whole PDF to Claude or Gemini by
excluding pdf-parser goes with it: the 4.2 shape is "pages": {"text": "OCR"} with the
VLM as recognizer, which sends rendered pages rather than the file.
Embedding filters and the image embedding parser deprecated (4.1.0+)
openai-embedding-filter, jina-embedding-filter and openai-image-embedding-parser are
deprecated and will be removed in 4.2.0. Configure the endpoint once under engines and bind
it with a TEXT or IMAGES inference binding; it embeds
the whole document tree in batched requests and puts a picture’s vector on the document it is
part of. Until then they keep working from the server-side config, with a WARN at startup. A
request can no longer name the filters in a per-request metadata-filters list (4.0.x
accepted a caller-supplied baseUrl there); the flat per-request
{"openai-embedding-filter": {"skipEmbedding": true}} form is unchanged.
PDF page settings become the pages block (4.1.0+)
What the PDF parser did with its pages was spread over ocr, imageStrategy and the parser’s
own flags. It is now one pages block, shared with every parser
that renders (the EMF/WMF parsers in 4.1): text (where a page’s text comes from: EXTRACT,
AUTO, EXTRACT_AND_OCR, OCR, and the new NONE), render (dpi, a box, a minimum, colour,
format, quality, maxImagePixels), ocr (maxPages, the AUTO verdict under auto),
inference and emit. The same block under parse-context is the default for every such
parser; a parser’s own pages overlays it.
The 4.0 spellings load as aliases, and a config dump and the XML converter write pages:
ocr.strategy is pages.text (NO_OCR is EXTRACT, OCR_ONLY is OCR,
OCR_AND_TEXT_EXTRACTION is EXTRACT_AND_OCR), ocr.strategyAuto is pages.ocr.auto,
ocr.maxPagesToOcr is pages.ocr.maxPages, the ocr image settings are pages.render,
ocr.renderingStrategy is the parser’s renderingStrategy, imageStrategy: RAW_IMAGES is
extractInlineImages and the RENDER_PAGES_* values are pages.emit.enabled. Two behaviours
change with the aliases: page renders are emitted at the end of each page whichever
RENDER_PAGES_* value was given (they preceded the text under RENDER_PAGES_BEFORE_PARSE),
and a page the parser does not read (maxPages) is not rendered. Two floors are new: a page
narrower or shorter than 2 pixels at the target dpi is not rendered, and an image under 2 x 2
pixels reaches no text recognizer or inference binding (pages.render.minWidth/minHeight,
_min-width/_min-height on a recognizer entry, minWidth/minHeight on a binding). A binding’s
minWidth: 0 restores 4.0; the 2-pixel floor before the recognizers stays whatever the entry says. A per-request ocr block now changes only the fields it names (4.0 replaced the whole
block). In Java, PDFParserConfig.getOcr() and getImageStrategy() are gone: getPages() is
the overlay, the aliases write to it, the deprecated setters stay.
For the general serialization model and how JSON configuration works, see Serialization and Configuration.
Full Configuration Example
A complete Tika 4.x JSON configuration file with the commonly configured parsers:
{
"parsers": [
{
"pdf-parser": {
"extractInlineImages": true,
"extractUniqueInlineImagesOnly": true,
"sortByPosition": true,
"maxMainMemoryBytes": 1000000000
}
},
{
"tesseract-ocr-parser": {
"language": "eng+fra",
"pageSegMode": "1",
"timeoutMillis": 300000,
"otherTesseractConfig": {
"textord_initialx_ile": "0.75",
"textord_noise_hfract": "0.15625"
}
}
},
{
"default-parser": {}
}
]
}
Metadata Key Changes
Tika 4.x prefixes all "user generated" metadata keys to prevent overwrites and improve
namespace clarity. Writing to a reserved tk: key by String name also changed: it
silently succeeded in 3.x and now throws.
See Metadata Changes in 4.x for complete details, including a full table of changes, the write-API/reserved-key-guard changes, and code migration examples.
API Changes
TikaConfig replaced by TikaLoader
TikaConfig has been removed. Use TikaLoader from tika-serialization instead.
3.x:
TikaConfig config = new TikaConfig(getClass().getClassLoader());
Parser parser = config.getParser();
Detector detector = config.getDetector();
AutoDetectParser autoDetect = new AutoDetectParser(config);
4.x:
// Default configuration (SPI-discovered components)
TikaLoader loader = TikaLoader.loadDefault(getClass().getClassLoader());
// Or from a JSON config file
TikaLoader loader = TikaLoader.load(Path.of("tika-config.json"));
// Access components
Parser parser = loader.loadParsers();
Detector detector = loader.loadDetectors();
Parser autoDetect = loader.loadAutoDetectParser();
ParseContext context = loader.loadParseContext();
TikaLoader is in the tika-serialization module. Add tika-serialization
as a dependency if you were previously only depending on tika-core.
See Serialization and Configuration for
the full TikaLoader API.
|
For simple use cases, the Tika facade and DefaultParser still work without
TikaLoader:
// Simple facade (unchanged from 3.x)
Tika tika = new Tika();
String text = tika.parseToString(file);
// Direct parser use (unchanged from 3.x)
Parser parser = new DefaultParser();
ExternalParser is configuration-only
ExternalParser still exists (org.apache.tika.parser.external.ExternalParser), but it is
no longer discovered from the classpath: CompositeExternalParser and
ExternalParsersFactory, which loaded tika-external-parsers.xml definitions from the
classpath automatically, are gone. Declare each external parser in your JSON config instead. See
External Parser Configuration
for details.
TikaInputStream no longer spools unless asked
A TikaInputStream wrapping a plain InputStream reads straight through; nothing is written to
disk until getFile()/getPath() is called, or enableRewind() starts caching.
|
|
See Spooling for detail.
EmbeddedDocumentExtractor is now stateless
|
If you call a concrete parser directly instead of going through
|
ParsingEmbeddedDocumentExtractor (and Tika Pipes' UnpackExtractor) no longer capture a
ParseContext at construction. Every method now takes the ParseContext of the enclosing
parse as a parameter, and a single shared instance is reused across parses instead of one
being built per parse.
3.x / early 4.x:
EmbeddedDocumentExtractor extractor = new ParsingEmbeddedDocumentExtractor(context);
if (extractor.shouldParseEmbedded(metadata)) {
extractor.parseEmbedded(tis, handler, metadata, outputHtml);
}
4.x:
EmbeddedDocumentExtractor extractor = ParsingEmbeddedDocumentExtractor.INSTANCE;
if (extractor.shouldParseEmbedded(metadata, context)) {
extractor.parseEmbedded(tis, handler, metadata, context, outputHtml);
}
This affects:
-
EmbeddedDocumentExtractor#shouldParseEmbedded(Metadata)→shouldParseEmbedded(Metadata, ParseContext). Any customEmbeddedDocumentExtractorimplementation must add the parameter. -
ParsingEmbeddedDocumentExtractor’s `ParseContextconstructor is gone — use theParsingEmbeddedDocumentExtractor.INSTANCEsingleton (orUnpackExtractor.INSTANCEin Tika Pipes). A subclass that calledsuper(context)should drop the constructor and readParseContextfrom the method parameter instead of a captured field. -
ParsingEmbeddedDocumentExtractor#checkEmbeddedLimits(ParseRecord)→checkEmbeddedLimits(ParseRecord, ParseContext), andisWriteFileNameToContent()→isWriteFileNameToContent(ParseContext). A subclass overriding either must add the parameter — without@Override, the old signature silently becomes a dead, unused overload instead of a compile error. -
EmbeddedDocumentExtractorFactory,EmbeddedDocumentByteStoreExtractorFactory,StandardExtractorFactory, and Tika Pipes'UnpackExtractorFactoryare deleted — there is no longer a per-parse object to build. Code that supplied a custom factory should instead bind anEmbeddedDocumentExtractorinstance directly:// 3.x/early 4.x context.set(EmbeddedDocumentExtractorFactory.class, new MyExtractorFactory()); // 4.x context.set(EmbeddedDocumentExtractor.class, MyExtractor.INSTANCE); -
EmbeddedDocumentUtil’s instance API is removed (the constructor, and the instance methods `getPasswordProvider(),getDetector(),getMimeTypes(),getExtension(TikaInputStream, Metadata),shouldParseEmbedded(Metadata),parseEmbedded(…)). Use the static replacements, which takeParseContextexplicitly:EmbeddedDocumentUtil.getDetector(context),EmbeddedDocumentUtil.getMimeTypes(context),EmbeddedDocumentUtil.getExtension(tis, metadata, context), or call the methods on theEmbeddedDocumentExtractorobtained fromEmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context)directly.getPasswordProvider()had no replacement added since it had no callers; usecontext.get(PasswordProvider.class).
Behavior change: parsing a concrete parser directly with a bare ParseContext
|
Calling a concrete parser directly (bypassing
If you rely on embedded documents being parsed, set |
Behavior change: identifying embedded files with a bare ParseContext
|
Some container parsers (
If you rely on embedded files being identified, set |
ParseContext config resolution is per component, not per config class
A JSON-configured component’s resolved config is no longer published under its config class.
ConfigDeserializer used to call context.set(configClass, config) so any component could
find it with parseContext.get(SomeConfig.class); that leaked one component’s settings to
every other component binding the same config class — the three VLM parsers all bind
VLMOCRConfig, so one provider’s base URL and API key reached the other two. Configs are now
cached by (component name, config class).
Two things change for callers:
-
parseContext.get(SomeConfig.class)no longer returns a JSON-resolved config. A third-party component that followed thePDFBoxRendererpattern — pulling its config off theParseContextby class — must now be handed its config explicitly. -
Precedence is inverted. When a key has a JSON config, that config wins over a programmatic
context.set(XConfig.class, …); in earlier 4.x builds the programmatic value won. A programmatic value is still honored for any key with no JSON config.
tika-grpc: generated Java classes moved package
tika.proto’s `java_package changed from org.apache.tika to
org.apache.tika.pipes.grpc.proto. With java_multiple_files = true this moves every
generated class — TikaGrpc, FetchAndParseRequest, FetchAndParseReply and the rest — so a Java gRPC client must update its imports:
// 3.x / earlier 4.x
import org.apache.tika.TikaGrpc;
import org.apache.tika.FetchAndParseRequest;
// 4.x
import org.apache.tika.pipes.grpc.proto.TikaGrpc;
import org.apache.tika.pipes.grpc.proto.FetchAndParseRequest;
This is a source break only. The proto package (tika) and the service name (Tika) are
unchanged, so the wire protocol is identical: existing binaries keep working against a 4.x
server, and clients generated for other languages need no change.
Timeout Model Changes
4.x replaces the previous ad hoc, per-parser timeout handling with a single unified model
(TimeoutLimits / ParseTimeout) shared across library use, tika-app --fork, and Tika
Pipes. This affects error handling (TikaTimeoutException is now a checked exception),
CLI flags (tika-app --fork-timeout was removed), and several parser/pipes config field
names (*TimeoutSeconds/*TimeoutMs → *TimeoutMillis, including a unit change for
Tess4J specifically). See
Timeouts: Upgrading from Tika 3.x for
the full list of behavioral changes and required config edits.
Deprecations and Removals
-
TikaConfig— replaced byTikaLoader -
Metadata#setAll(Properties)— raw map write that bypassed the reserved-key guard; useMetadata#putAll(Metadata)instead (see Metadata Changes in 4.x) -
CompositeExternalParser— external parsers now require explicit JSON configuration -
ExternalParsersFactoryand XML-based external parser auto-discovery -
DOM-based OOXML extractors (
XWPFWordExtractorDecorator,XSLFPowerPointExtractorDecorator) — SAX-based extractors are now the only implementation -
TikaMimeKeys— the interface is deleted outright, so direct references break too (not just the constants formerly inherited byMetadata) -
Metadatano longer implementsCreativeCommons,Geographic,HttpHeaders,Message,ClimateForecast(renamed fromClimateForcast), orTIFF— reference the interface’s constants directly instead of the inheritedMetadataconstant (e.g.Geographic.LATITUDE) -
CreativeCommons— deleted outright: no parser ever produced its three keys (License-Url,License-Location,Work-Type), and nothing consumed them -
ParserUtils.EMBEDDED_PARSER— useTikaCoreProperties.EMBEDDED_PARSER(it was a same-Property alias) -
package
org.apache.tika.metadata.writefilter→org.apache.tika.metadata.writelimiter(the classes were renamed Filter → Limiter earlier in 4.0.0; the package now matches) -
ClimateForecastkeys move undercf:(cf:history,cf:comment, …; suffixes keep the CF convention’s verbatim spellings) -
TikaCoreProperties.TIKA_CONTENT_HANDLER— useTIKA_CONTENT_HANDLER_TYPE -
OfficeOpenXMLCore.SUBJECT— useDublinCore#SUBJECT -
XMPDM.ChannelTypePropertyConverter— experimental, no replacement -
8 deprecated
IPTCproperties:URGENCY,CATEGORY,SUPPLEMENTAL_CATEGORIES(use thePhotoshopequivalents),DIGITAL_SOURCE_FILE_TYPE(IPTC no longer recommends the field), and the four*_WRONG_CASEfields (use the correctly-cased sibling, e.g.IMAGE_SUPPLIER_ID) -
Property.PropertyType.STRUCTUREandProperty.ValueType.{LOCALE, MIME_TYPE, PROPER_NAME, URL, XPATH}— dead enum constants, never produced by anyPropertyfactory -
Property.internalClosedChoise/internalOpenChoise/externalClosedChoise/externalOpenChoise— renamed to…Choice(typo-fix rename, no forwarders) -
EmbeddedDocumentExtractorFactory,EmbeddedDocumentByteStoreExtractorFactory,StandardExtractorFactory,UnpackExtractorFactory— deleted; bind anEmbeddedDocumentExtractorinstance directly instead (see EmbeddedDocumentExtractor is now stateless above) -
ParsingEmbeddedDocumentExtractor(ParseContext)constructor — removed; use theParsingEmbeddedDocumentExtractor.INSTANCEsingleton -
EmbeddedDocumentUtil’s instance API (constructor and all instance methods) — removed; use the static equivalents, which now take `ParseContextexplicitly