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
- 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. get(byte[]) and getFromContainer no longer declare
IOException. enableRewind() now throws a checked IOException when bytes have
already been read.
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:
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