Skip to content

feat(api)!: replace DoclingServeApiBuilderFactory with DoclingServeApiProvider - #696

Merged
edeandrea merged 1 commit into
docling-project:mainfrom
edeandrea:spi-config-provider
Sep 23, 2026
Merged

edeandrea merged 1 commit into
docling-project:mainfrom
edeandrea:spi-config-provider

Conversation

@edeandrea

Copy link
Copy Markdown
Contributor

Why

DoclingServeApiBuilderFactory handed out a DoclingApiBuilder, an interface that every implementation had to implement. That interface played two roles: the configuration carrier for callers, which must grow, and the extension contract for implementors, which must never change. So each new option was either a breaking change or a default method throwing UnsupportedOperationException, as asyncExecutor in #691 had to be.

This PR splits the two roles. Providers now receive an immutable configuration object owned by docling-serve-api, so options can be added without breaking implementations.

What changes

New SPI (docling-serve-api)

  • DoclingServeApiProvider: create(DoclingServeApiConfig) returns the API. Discovered with ServiceLoader, exactly one provider must be available.
  • DoclingServeApiConfig: a final, immutable class. Every option has a typed ConfigOption constant and an accessor, and the config records which options were explicitly set.
  • DoclingServeApiBuilder: returned by DoclingServeApi.builder(). It collects a config and hands it to the provider in build().
  • unsupportedOptions(): a provider can declare options it doesn't honor as IGNORE, WARN or FAIL. Only options the caller explicitly set are enforced, so declaring an option doesn't affect callers who never touch it. FAIL throws an UnsupportedConfigurationException.
  • DoclingServeApi.config(): new abstract method returning the configuration an API runs with. An API is copied with api.config().toBuilder().

Deprecated for removal, with a fallback

  • DoclingServeApiBuilderFactory, DoclingServeApi.DoclingApiBuilder and DoclingServeApi.toBuilder().
  • A legacy factory is still used when no provider is found, through an adapter that replays the options onto the old builder. Its replay list is frozen, so every option added after the deprecation, starting with asyncExecutor, makes build() fail through a legacy factory instead of being silently dropped.
  • If both a provider and a legacy factory are present, the provider is used and a warning names the ignored factory.

Reference client (docling-serve-client)

  • New DoclingServeClientProvider, registered in place of DoclingServeClientBuilderFactory.
  • The client builder keeps a DoclingServeApiBuilder for the shared options instead of duplicating them as fields, and gains config(DoclingServeApiConfig). The client stores the config and returns it from config().
  • New DoclingServeClient.builder(), which detects Jackson 2 or 3, for client-specific settings such as httpClientBuilder. DoclingServeJackson2Client.builder() and DoclingServeJackson3Client.builder() are now public, for jsonParser. Callers only need to know their Jackson version when writing Jackson code.
  • DoclingServeClientBuilderFactory is deprecated for removal and delegates to DoclingServeClient.builder().

Other changes

  • Removed: the unreleased default DoclingApiBuilder.asyncExecutor() from feat(client): support custom Executor for async operations #691. asyncExecutor is now a config option. It stays @Nullable rather than Optional, and unset still means the 1-arg CompletableFuture overloads, never ForkJoinPool.commonPool().
  • Fixed: toBuilder() on the Jackson clients dropped connectTimeout and readTimeout, and the copy stopped following redirects. Proxy, SSL context and authenticator still aren't copied, since HttpClient can't be rebuilt from an instance; this is documented.
  • Dependency: slf4j-api moves from docling-serve-client to docling-serve-api, which now logs the WARN and legacy-factory warnings.
  • Version: set to 0.7.0 in .github/project.yml.

Breaking changes

  • DoclingServeApi.builder() returns a DoclingServeApiBuilder instead of the implementation's builder (<T, B> B). Fluent chains and var declarations are unaffected; code declaring the builder type must change, and client-specific settings move to DoclingServeClient.builder().
  • DoclingServeApi has a new abstract config(). Implementations must add it; binaries compiled against an earlier version throw AbstractMethodError if it's called.
  • The reference client's builder setters validate values immediately, so an invalid value such as connectTimeout(Duration.ZERO) now throws from the setter instead of from build().
  • config() on the reference client reports only the options that were explicitly set, and the base URL as given.

Documentation

  • New migration guide, docling-serve/serve-api-provider-migration.md, in the nav: before/after examples for the provider, service registration, unsupportedOptions(), config(), callers of the builder, and tests to check the migration. Every Java snippet on the page was compiled.
  • serve-api.md, serve-client.md, whats-new.md and CLAUDE.md updated.

Testing

  • docling-serve-api: 237 tests pass. New tests cover the config, the builder, unsupportedOptions() enforcement, provider resolution (new, legacy, both, none, several) and the legacy adapter. Guard tests fail if a ConfigOption constant, its accessor and its builder setter get out of sync.
  • docling-serve-client: the provider, builder, deprecated-factory, config and async-executor suites pass for both Jackson 2 and Jackson 3 (38 tests). A guard test fails if a shared setter is missing from the client builder.
  • The new tests were mutation-checked: removing the behavior each one guards makes it fail.
  • Native image: :docling-native-tests:nativeClientTest passes with GraalVM 25 for Jackson 2 and Jackson 3.
  • Not run locally: the full docling-serve-client suite, which needs Docker (Testcontainers). I'm relying on CI for it.

Follow-up

A separate issue will track removing the deprecated types once third-party implementations have migrated.

…iProvider

DoclingServeApiBuilderFactory handed out a DoclingApiBuilder, an interface
every implementation had to implement. Each new configuration option was
either a breaking change or a default method throwing
UnsupportedOperationException.

The new DoclingServeApiProvider SPI instead receives an immutable
DoclingServeApiConfig, a final class owned by docling-serve-api, so options
can be added without breaking implementations.

- DoclingServeApi.builder() returns a DoclingServeApiBuilder collecting a
  DoclingServeApiConfig; build() hands it to the single available provider
- Providers declare the options they don't honor via unsupportedOptions()
  (IGNORE, WARN or FAIL), enforced only for explicitly set options
- DoclingServeApi gains an abstract config(); an API is copied with
  api.config().toBuilder()
- DoclingServeApiBuilderFactory, DoclingApiBuilder and
  DoclingServeApi.toBuilder() are deprecated for removal. Legacy factories
  are still used when no provider is found, but options added after the
  deprecation, such as asyncExecutor, fail through them
- docling-serve-client provides DoclingServeClientProvider. The client
  builder keeps a DoclingServeApiBuilder instead of duplicating every
  option, and validates values when they are set
- DoclingServeClient.builder() detects Jackson 2 or 3 for client-specific
  settings; the builder() methods of the Jackson clients are now public,
  and DoclingServeClientBuilderFactory is deprecated for removal
- toBuilder() of the reference client keeps the timeouts and the redirect
  policy
- slf4j-api moves from docling-serve-client to docling-serve-api
- Remove the unreleased default DoclingApiBuilder.asyncExecutor() from
  docling-project#691: asyncExecutor is now a DoclingServeApiConfig option
- Add a migration guide, and set the release version to 0.7.0

BREAKING CHANGE: DoclingServeApi.builder() now returns a
DoclingServeApiBuilder instead of the builder of the implementation, and
DoclingServeApi has a new abstract config() method.

Signed-off-by: Eric Deandrea <eric.deandrea@ibm.com>
@edeandrea edeandrea added module:docling-serve-api The docling-serve-api module module:docling-serve-client The docling-serve-client module area:docling-serve Something related to docling-serve enhancement New feature or request labels Sep 23, 2026
@edeandrea

Copy link
Copy Markdown
Contributor Author

Follow-up to remove the deprecated types: #697

@edeandrea
edeandrea enabled auto-merge (squash) September 23, 2026 23:13
@edeandrea
edeandrea merged commit fec64b5 into docling-project:main Sep 23, 2026
28 checks passed
@github-actions

Copy link
Copy Markdown

:java_duke: JaCoCo coverage report

Overall Project 49.58% 🔴

There is no coverage information present for the Files changed

@github-actions

Copy link
Copy Markdown
TestsPassed ✅SkippedFailed
Gradle Test Results (all modules & JDKs)2084 ran2084 passed0 skipped0 failed
TestResult
No test annotations available

@github-actions

Copy link
Copy Markdown

HTML test reports are available as workflow artifacts (zipped HTML).

• Download: Artifacts for this run

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

Labels

area:docling-serve Something related to docling-serve enhancement New feature or request module:docling-serve-api The docling-serve-api module module:docling-serve-client The docling-serve-client module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant