How do you expose a custom model to Tooling API clients using ToolingModelBuilder and the ToolingModelBuilderRegistry?
answer
- ToolingModelBuilder: canBuild + buildAll
- canBuild matches model FQ name
- registry.register(...) from a plugin
- @Inject ToolingModelBuilderRegistry
- model interface on both classpaths, serializable
basics
~10 sWrite 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 sTo 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 linespublic 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
Awareness that custom models exist beyond the built-in ones is enough.
Describe the ToolingModelBuilder canBuild/buildAll pair and registry.register.
Cover @Inject of the registry, serialization constraints, and where the plugin is applied (init script for cross-build).
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