skip to content

How do you expose a custom model to Tooling API clients using ToolingModelBuilder and the ToolingModelBuilderRegistry?

level: seniorimportance: should knowfreq 30%

answer

  1. ToolingModelBuilder: canBuild + buildAll
  2. canBuild matches model FQ name
  3. registry.register(...) from a plugin
  4. @Inject ToolingModelBuilderRegistry
  5. model interface on both classpaths, serializable

basics

~10 s

Write a ToolingModelBuilder that returns your model for a given model name, register it in a plugin via the ToolingModelBuilderRegistry, and clients fetch it with connection.getModel(YourModel.class). The model interface must be on both classpaths.

solid answer

~40 s

To surface build-specific data to TAPI clients you implement a **`ToolingModelBuilder`** with two methods: `canBuild(String modelName)` — true when `modelName` equals your model interface's fully-qualified name — and `buildAll(String modelName, Project project)` which constructs and returns a serializable model object. You **register** it from a plugin by injecting the **`ToolingModelBuilderRegistry`** and calling `registry.register(new MyModelBuilder())`. The plugin is typically applied via an init script or settings so it's present during the client's configuration. The **model interface** must be visible to both sides; clients depend on the same interface artifact and call `connection.getModel(MyCustomModel.class)`. The returned object is serialized, so expose only simple, serializable types (Strings, collections, other model interfaces) — not live Gradle objects.

code

java · 17 lines
java
public class CustomModelPlugin implements Plugin<Project> {
    private final ToolingModelBuilderRegistry registry;
    @Inject
    public CustomModelPlugin(ToolingModelBuilderRegistry registry) {
        this.registry = registry;
    }
    @Override public void apply(Project project) {
        registry.register(new ToolingModelBuilder() {
            @Override public boolean canBuild(String modelName) {
                return modelName.equals(CustomModel.class.getName());
            }
            @Override public Object buildAll(String modelName, Project project) {
                return new DefaultCustomModel(project.getName());
            }
        });
    }
}

go deeper

for a junior

Awareness that custom models exist beyond the built-in ones is enough.

for a middle

Describe the ToolingModelBuilder canBuild/buildAll pair and registry.register.

for a senior

Cover @Inject of the registry, serialization constraints, and where the plugin is applied (init script for cross-build).

for a principal

Discuss the model interface as a versioned contract shared with many client/Gradle versions and its evolution/governance.

## The problem Built-in models (`GradleProject`, `EclipseProject`, `IdeaProject`) cover IDE needs but not *your* tool's needs. A custom model lets a plugin publish arbitrary, computed build information to TAPI clients. ## The three pieces 1. **A model interface** — a plain Java interface (the *contract*). It must live in an artifact both the plugin and the client can see, because TAPI serializes the implementation and reconstructs a proxy of this interface on the client. ```java public interface CustomModel extends Serializable { String getProjectName(); List<String> getApplicationModules(); } ``` 2. **A `ToolingModelBuilder`** — knows how to build it: ```java public class CustomModelBuilder implements ToolingModelBuilder { @Override public boolean canBuild(String modelName) { return modelName.equals(CustomModel.class.getName()); } @Override public Object buildAll(String modelName, Project project) { return new DefaultCustomModel( project.getName(), project.getSubprojects().stream().map(Project::getName).collect(toList())); } } ``` `canBuild` matches by the model's **fully-qualified name** (the string the client's `getModel(Class)` sends). `buildAll` runs in the daemon with full access to the configured `Project`, and returns a serializable value object. 3. **Registration via the registry** — a plugin injects `ToolingModelBuilderRegistry` and registers the builder: ```java public class CustomModelPlugin implements Plugin<Project> { private final ToolingModelBuilderRegistry registry; @Inject public CustomModelPlugin(ToolingModelBuilderRegistry registry) { this.registry = registry; } @Override public void apply(Project project) { registry.register(new CustomModelBuilder()); } } ``` ## Client side ```java CustomModel model = connection.getModel(CustomModel.class); model.getApplicationModules().forEach(System.out::println); ``` Gradle finds the registered builder whose `canBuild` returns true for `CustomModel`'s name, calls `buildAll`, serializes the result, and the client navigates a proxy. ## Serialization constraints The returned object crosses a process and version boundary. Expose only: - primitives, `String`, `File`, enums, - collections of the above or of other model interfaces, - nested model interfaces. Never return live Gradle types (`Task`, `Configuration`, `Project`) — they aren't meaningfully serializable across the boundary. Keep the model interface stable; clients may run against many Gradle versions. ## Where the plugin is applied For IDE-style cross-build models the registering plugin is often applied through an **init script** so it is present regardless of the target build's own plugins. For project-scoped models, a normal plugin applied in the build works.

  • Why must the model interface be available on both the plugin and the client classpath?
    TAPI serializes the builder's result and reconstructs a proxy of the interface on the client; both sides need the interface type to talk about the same contract.
  • What does canBuild compare against?
    The fully-qualified class name of the requested model — the string derived from the client's getModel(Class) call — typically YourModel.class.getName().
  • Can buildAll return a Task or Configuration directly?
    No. The result is serialized across a process/version boundary; expose only serializable value types, not live Gradle objects.

saying these in an interview costs you the question

  • Returning live Gradle objects (Task/Project/Configuration) from buildAll
  • Matching canBuild against the simple name instead of the FQ name
  • Forgetting that the model interface artifact must be shared with clients

context