Most services an agent needs to call already describe themselves with an OpenAPI document. It is tempting to feed the whole spec to the agent and let the model call whatever it likes. That works in a demo and fails in production, where specs are large, schemas are complicated and some endpoints delete things.

This article covers what the Java Agent Development Kit actually provides for OpenAPI, how to build a spec-driven toolset on its real tool interfaces, and the engineering around it: schema translation, authentication, curation, error handling and tests. It assumes you know how a single tool works; if not, start with writing a FunctionTool.

Advertisement

What adk-java ships today

Start with an accurate inventory, because the documentation can mislead. The adk-java README lists "OpenAPI specs" among its tool sources. As of the main branch on 2026-10-01, however, the core com.google.adk.tools package has no general-purpose OpenAPI toolset. The Python ADK has one, OpenAPIToolset, which generates a RestApiTool per operation, and Java readers who have seen Python examples often go looking for its twin. It is not there.

What Java does have is ApplicationIntegrationToolset in com.google.adk.tools.applicationintegrationtoolset. It targets Google Cloud Application Integration and Integration Connectors: it fetches the OpenAPI spec of an integration or connection, and each IntegrationConnectorTool derives its function declaration from that spec's operation and request schema. If your target system is reachable through Integration Connectors, that is the supported path. For an arbitrary REST service with its own spec, you write a small toolset yourself. That is less work than it sounds, because the extension points are clean.

APIs move, so check the repository before relying on this: a later release may add a general OpenAPI toolset, and if it does you should prefer it to hand-written code.

The two interfaces you build on

Every ADK Java tool extends BaseTool. A subclass passes a name and description to the constructor, overrides declaration() to return an Optional<FunctionDeclaration> (the schema the model sees), and overrides runAsync(Map<String, Object> args, ToolContext toolContext), which returns an RxJava Single<Map<String, Object>>. The map becomes the function response sent back to the model.

A group of tools that share configuration, such as one base URL and one credential, belongs in a BaseToolset. It is an interface with one method to implement, Flowable<BaseTool> getTools(ReadonlyContext readonlyContext), plus close() for releasing resources, and a default isToolSelected helper that applies a filter given as a list of tool names or a ToolPredicate. LlmAgent.builder().tools(...) accepts tools and toolsets together; the agent separates the toolsets out and asks each one for its tools. Because getTools receives a context, a toolset can expose different operations to different users or sessions.

Advertisement

Architecture

Startup: the spec becomes tools. Each turn: the model picks one, the tool makes the HTTP call.openapi.yamlpinned versionswagger-parserresolve refsAllowlist filteroperationIdsRestOperationToolone per opOpenApiToolsetgetTools(ctx)LlmAgenttools(...)ModelFunctionDeclarationsdeclarationsrunAsync(args, ctx)validate, authorisefunction callHttpClienttimeout, retry policyRemote REST APIResponse trimmed and returnedas Map to the model
Parsing and filtering happen once at startup. At run time the model sees only declarations; the tool owns validation, credentials, the HTTP call and trimming the response.

The data flow has two phases. At startup, load a pinned copy of the spec, resolve its $ref pointers, keep only allowlisted operations, and build one tool per operation. At run time, the agent sends the declarations with each model request; when the model returns a function call, ADK dispatches it to the matching tool's runAsync, which validates arguments, attaches credentials, makes the request and returns a compact result. How that dispatch works internally is covered in runtime tool dispatch mechanics.

Building the toolset

The sketch below uses the open-source swagger-parser library (io.swagger.parser.v3:swagger-parser) to read the spec, Jackson to build JSON Schema, and java.net.http.HttpClient for calls. It is deliberately small; it handles path, query and JSON body parameters and leaves headers, cookies and multipart out.

public final class OpenApiToolset implements BaseToolset {
  private final List<BaseTool> tools = new ArrayList<>();

  public OpenApiToolset(String specLocation, String baseUrl, Set<String> allowedOps,
                        HttpClient http, CredentialSource creds) {
    ParseOptions opts = new ParseOptions();
    opts.setResolve(true);
    opts.setResolveFully(true);               // inline $ref so each tool is self-contained
    SwaggerParseResult result = new OpenAPIV3Parser().readLocation(specLocation, null, opts);
    if (result.getOpenAPI() == null) {
      throw new IllegalStateException("Spec did not parse: " + result.getMessages());
    }
    result.getOpenAPI().getPaths().forEach((path, item) ->
        item.readOperationsMap().forEach((method, op) -> {
          if (op.getOperationId() != null && allowedOps.contains(op.getOperationId())) {
            tools.add(new RestOperationTool(baseUrl, path, method.name(), op, http, creds));
          }
        }));
    if (tools.size() != allowedOps.size()) {
      throw new IllegalStateException("Allowlisted operation missing from spec");
    }
  }

  @Override
  public Flowable<BaseTool> getTools(ReadonlyContext ctx) {
    return Flowable.fromIterable(tools);
  }

  @Override
  public void close() {}
}

Failing at startup when an allowlisted operation has disappeared is intentional. A renamed operationId in a new spec version would otherwise silently remove a capability, and the agent would start apologising for something it used to do.

Each operation becomes a tool. The declaration is built once, in the constructor:

final class RestOperationTool extends BaseTool {
  private static final ObjectMapper JSON = new ObjectMapper();
  private final String baseUrl, path, method;
  private final Operation op;
  private final FunctionDeclaration decl;
  private final HttpClient http;
  private final CredentialSource creds;

  RestOperationTool(String baseUrl, String path, String method, Operation op,
                    HttpClient http, CredentialSource creds) {
    super(sanitize(op.getOperationId()), describe(op));   // letters, digits, underscores
    this.baseUrl = baseUrl; this.path = path; this.method = method;
    this.op = op; this.http = http; this.creds = creds;
    this.decl = FunctionDeclaration.builder()
        .name(name())
        .description(description())
        .parameters(Schema.fromJson(parametersSchema(op)))
        .build();
  }

  @Override
  public Optional<FunctionDeclaration> declaration() { return Optional.of(decl); }

  @Override
  public Single<Map<String, Object>> runAsync(Map<String, Object> args, ToolContext ctx) {
    return Single.fromCallable(() -> {
      HttpRequest req = buildRequest(args, creds.tokenFor(ctx));   // validates args first
      HttpResponse<String> resp = http.send(req, HttpResponse.BodyHandlers.ofString());
      return toToolResult(resp);
    }).subscribeOn(Schedulers.io());
  }
}

parametersSchema folds the operation's path and query parameters and its JSON request body into one object schema, with the spec's required lists carried across, and serialises it with Jackson. Schema.fromJson is the same call the Integration Connectors tool uses. describe combines the operation's summary and description and should add anything the model needs to choose correctly, such as "use this only after the user has confirmed the order".

Wiring it into an agent is one line:

LlmAgent agent = LlmAgent.builder()
    .name("orders_assistant")
    .model(MODEL)                 // a current Gemini model ID
    .instruction("Help customers check and change orders. Never cancel without explicit confirmation.")
    .tools(new OpenApiToolset("specs/orders-v3.yaml", "https://orders.internal",
        Set.of("getOrder", "listOrders", "updateShippingAddress"), http, creds))
    .build();

Translating schemas the model can use

OpenAPI schemas are richer than what function-calling models accept. Gemini's function declarations take an OpenAPI-style subset, and constructs such as deeply nested $ref chains, recursive types, oneOf with discriminators, and additionalProperties maps are poorly supported or rejected. Rather than relying on specific limits, which change between model versions, translate conservatively:

  • Resolve every reference at load time so each declaration is self-contained.
  • Flatten: if the body is an object with a few fields, expose those fields at the top level and rebuild the body in buildRequest.
  • Drop read-only fields from request schemas and anything the server fills in, such as IDs and timestamps.
  • Replace polymorphic schemas with an explicit enum field plus the union of the relevant properties, and validate the combination in code.
  • Keep descriptions, enums and formats, which help the model more than structure does. Remove examples that contain real-looking personal data.
  • Sanitise names: an operationId can contain characters a function name may not, so map it to letters, digits and underscores and keep a lookup table.

Treat the generated declaration as a reviewed artefact. Print every declaration at build time and diff it in code review, because a change to the upstream spec changes what the model is told.

Authentication and authority

Never put credentials in the declaration, and never let the model supply them as arguments. The tool attaches them in runAsync from a source the model cannot influence. Decide first whose authority the call carries. A service account is simple, but then the agent can read any customer's order. Delegated user credentials, an OAuth access token for the signed-in user taken from session state via ToolContext, limit the blast radius to what that user could do anyway.

Add a check in the tool, not just in the prompt, for any argument that selects whose data is touched. If the session belongs to customer 42 and the model asks for getOrder(orderId=9001), the tool should confirm order 9001 belongs to customer 42 or return an error. The patterns are covered in authorization at the agent boundary. For state-changing operations, ToolContext.requestConfirmation lets a tool pause for an explicit user approval before acting.

Curate: fewer, better tools

Exposing every operation in a 200-endpoint spec gives the model 200 near-synonyms to choose between, uses a large share of the context window for declarations, and hands it destructive endpoints it never needed. Tool choice accuracy drops as the menu grows. Curation is the single most effective improvement.

DecisionRecommended defaultWhy
Which operationsAn explicit allowlist of operationIdsNew endpoints in a spec update are not exposed by accident
Destructive verbsExclude DELETE; gate PUT, PATCH and POST with confirmationA model error becomes an incident only if the tool can act
GranularityWrap multi-step workflows in one FunctionToolThree API calls the model must sequence fail more often than one
Per-user exposureFilter in getTools using the contextAdmins and customers should not see the same menu
Spec sourceA pinned file in the repositoryA live URL changes your agent's tools without a deploy

Responses, errors and timeouts

Return what the model needs and nothing else. A 40 KB order document wastes context and can carry instructions planted in free-text fields; pick fields with a projection, cap list lengths and say when results were truncated. Treat response text as untrusted data, since a product description or support note can contain a prompt injection.

Turn HTTP failures into structured results the model can reason about, such as {"error": "not_found", "retryable": false}, rather than throwing. A thrown exception ends the turn; a structured error lets the agent tell the user or try a different operation. Retry only idempotent methods, with a cap, and put a timeout on every request. Tool timeout handling covers the budget arithmetic. Log the operationId, status, latency and a request ID for every call, so an odd agent answer can be traced to the API call behind it.

Testing the integration

  • Spec contract test: load the pinned spec in CI and assert the allowlisted operations exist with the expected parameters. This catches upstream renames before users do.
  • Declaration snapshot: serialise every generated FunctionDeclaration and compare it with a checked-in snapshot.
  • Tool unit tests: call runAsync directly against a mock HTTP server and check request construction, authentication headers, error mapping and truncation.
  • Authorisation tests: attempt cross-user access through the tool and assert it is refused.
  • Agent evaluations: a small set of conversations that should pick each tool, and some that should pick none, run whenever the spec, descriptions or model change.

Trade-offs: spec-driven or hand-written

Generating tools from a spec scales to many operations and tracks API changes cheaply, but declarations inherit the spec's naming and descriptions, which were written for programmers rather than models. Hand-written FunctionTool wrappers around a generated client take more code, but let you design the interface the model sees: fewer parameters, clearer names, workflows instead of endpoints. A common middle ground is to generate tools for read operations and hand-write the few that change state. A third option is to put the API behind an MCP server and attach it with ADK's MCP toolset, which moves the translation out of your agent process; it adds a hop and a component to operate.

What to do next

  1. Check the adk-java release notes for a general OpenAPI toolset; if one has landed, evaluate it before writing your own.
  2. If your system is behind Integration Connectors, try ApplicationIntegrationToolset first.
  3. Pin the spec in your repository and write an allowlist of five or fewer operations to start.
  4. Implement the toolset above, print every declaration, and review them as you would a public API.
  5. Attach credentials in the tool from session state, add ownership checks, and require confirmation for state-changing calls.
  6. Map errors to structured results, set timeouts, trim responses, and log every call with its operationId.
  7. Add the contract, snapshot and authorisation tests, plus a short agent evaluation, to CI.
Key takeaway: On adk-java main, OpenAPI support means ApplicationIntegrationToolset for Integration Connectors; for any other REST API you build a small BaseToolset that turns allowlisted operations into BaseTool instances. Pin the spec, translate schemas conservatively, attach credentials and ownership checks in code, gate destructive calls, return compact structured results, and test the declarations as carefully as the HTTP calls.