What is A2UI?
A2UI is an open-source protocol that enables AI agents to generate rich, interactive user interfaces by sending declarative JSON descriptions to client applications. This article focuses on implementing A2UI servers using Java with Spring Boot and the a2ajava framework.
Current Status: A2UI v0.8 is in Public Preview, meaning the specification and implementations are functional but still evolving.
My Implementation: Java + Spring Boot + a2ajava
I'm building A2UI support using:
- Backend: Spring Boot 3.2.4 with Java 17+
- Framework: a2ajava 1.0.0 (Actions for AI Java)
- Architecture: Annotation-driven agents and actions
- Deployment: HuggingFace Spaces for the backend
The key advantage: Java's type safety ensures correct A2UI message structure at compile time.
Front end (angular) - https://github.com/vishalmysore/simplea2ui
Backend (Pure Java)- https://github.com/vishalmysore/springa2ui
Building A2UI with Java: Core Concept
In my Java implementation, A2UI messages are built as Map<String, Object> structures that get serialized to JSON:
@Service
@Agent(groupName = "compareCar", groupDescription = "compare 2 cars")
public class CompareCarService implements A2UIDisplay {
@Action(description = "compare 2 cars")
public Object compareCar(String car1, String car2) {
// Business logic
String betterCar = determineWinner(car1, car2);
// Check if client requested A2UI
if(isUICallback(callback)) {
return createComparisonUI(car1, car2, betterCar);
}
return betterCar + " is better"; // Plain text fallback
}
}
The separation:
- Backend (Java): Decides WHAT to display, generates A2UI JSON
- Frontend (any client): Renders components using native widgets
Java Implementation with a2ajava Framework
1. Annotation-Driven Architecture
Define agents using @Agent and actions using @Action:
@Service
@Agent(groupName = "whatThisPersonFavFood",
groupDescription = "Find what food a person likes")
public class SimpleService implements A2UIDisplay {
private ActionCallback callback;
private AIProcessor processor;
@Action(description = "Get the favourite food of a person")
public Object whatThisPersonFavFood(String name) {
String favFood = lookupFavoriteFood(name);
if(callback != null && callback.getType().equals(CallBackType.A2UI.name())) {
return createFavoriteFoodUI(name, favFood);
}
return favFood;
}
}
2. Building A2UI Components in Java
I created a utility interface A2UIDisplay with helper methods:
public interface A2UIDisplay {
// Create text components
default Map<String, Object> createTextComponent(String id, String text, String usageHint) {
Map<String, Object> component = new HashMap<>();
component.put("id", id);
Map<String, Object> textComponent = new HashMap<>();
Map<String, Object> textProps = new HashMap<>();
textProps.put("text", new HashMap<String, Object>() {{
put("literalString", text);
}});
if (usageHint != null) {
textProps.put("usageHint", usageHint);
}
textComponent.put("Text", textProps);
component.put("component", textComponent);
return component;
}
// Create TextField with data binding
default Map<String, Object> createTextFieldComponent(String id, String label, String dataPath) {
Map<String, Object> component = new HashMap<>();
component.put("id", id);
Map<String, Object> textFieldProps = new HashMap<>();
textFieldProps.put("label", new HashMap<String, Object>() {{
put("literalString", label);
}});
textFieldProps.put("text", new HashMap<String, Object>() {{
put("path", dataPath);
}});
component.put("component", new HashMap<String, Object>() {{
put("TextField", textFieldProps);
}});
return component;
}
// Create Button with context
default Map<String, Object> createButtonComponent(String id, String buttonText,
String actionName,
Map<String, String> contextBindings) {
Map<String, Object> component = new HashMap<>();
component.put("id", id);
Map<String, Object> buttonProps = new HashMap<>();
buttonProps.put("child", id + "_text");
Map<String, Object> action = new HashMap<>();
action.put("name", actionName);
if (contextBindings != null && !contextBindings.isEmpty()) {
List<Map<String, Object>> context = new ArrayList<>();
for (Map.Entry<String, String> binding : contextBindings.entrySet()) {
context.add(new HashMap<String, Object>() {{
put("key", binding.getKey());
put("value", new HashMap<String, Object>() {{
put("path", binding.getValue());
}});
}});
}
action.put("context", context);
}
buttonProps.put("action", action);
component.put("component", new HashMap<String, Object>() {{
put("Button", buttonProps);
}});
return component;
}
}
3. Creating Complete A2UI Messages
private Map<String, Object> createFavoriteFoodUI(String name, String favFood) {
List<Map<String, Object>> components = new ArrayList<>();
// Add title
components.add(createTextComponent("title", "Favorite Food Finder", "h2"));
// Add result
components.add(createTextComponent("result",
name + "'s favorite food is: " + favFood + " 😋", "body"));
// Add form
components.add(createTextFieldComponent("name_input",
"Person's Name", "/form/name"));
// Add button with context
Map<String, String> contextBindings = new HashMap<>();
contextBindings.put("name", "/form/name");
components.add(createButtonComponent("submit_button",
"Find Favorite Food", "whatThisPersonFavFood", contextBindings));
// Add button text child
components.add(createTextComponent("submit_button_text", "Find Favorite Food"));
// Initialize data model
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("/form/name", "");
return buildA2UIMessageWithData("favorite_food", "root", components, dataModel);
}
4. Data Model in Java (Adjacency List Format)
default Map<String, Object> createDataModelUpdateWithValues(String surfaceId,
Map<String, Object> initialValues) {
Map<String, Object> dataModelUpdate = new HashMap<>();
dataModelUpdate.put("surfaceId", surfaceId);
List<Map<String, Object>> contents = new ArrayList<>();
if (initialValues != null && !initialValues.isEmpty()) {
// Group paths by root key
Map<String, List<Map.Entry<String, Object>>> groupedByRoot = new LinkedHashMap<>();
for (Map.Entry<String, Object> entry : initialValues.entrySet()) {
String path = entry.getKey();
String[] parts = path.split("/");
if (parts.length >= 2) {
String rootKey = parts[1];
groupedByRoot.computeIfAbsent(rootKey, k -> new ArrayList<>()).add(entry);
}
}
// Build adjacency list format
for (Map.Entry<String, List<Map.Entry<String, Object>>> rootEntry : groupedByRoot.entrySet()) {
Map<String, Object> rootItem = new HashMap<>();
rootItem.put("key", rootEntry.getKey());
List<Map<String, Object>> valueMap = new ArrayList<>();
for (Map.Entry<String, Object> valueEntry : rootEntry.getValue()) {
String fullPath = valueEntry.getKey();
String[] pathParts = fullPath.split("/");
if (pathParts.length == 3) {
Map<String, Object> valueItem = new HashMap<>();
valueItem.put("key", pathParts[2]);
valueItem.put("valueString", valueEntry.getValue());
valueMap.add(valueItem);
}
}
rootItem.put("valueMap", valueMap);
contents.add(rootItem);
}
}
dataModelUpdate.put("contents", contents);
return dataModelUpdate;
}
Why Java for A2UI?
1. Type Safety
Java's type system ensures correct A2UI message structure at compile time. No runtime surprises from malformed JSON.
2. Spring Boot Integration
- Dependency injection for services
- Built-in REST controllers
- Easy deployment and scaling
- Production-ready features (metrics, health checks, etc.)
3. Reusable Components
The A2UIDisplay interface provides helper methods that all services can use, reducing code duplication.
4. Dual-Mode Support
Same backend code serves both A2UI-enabled clients and plain text clients through callback detection.
A2UI Protocol Essentials (Java Perspective)
Message Types
When building A2UI with Java, you generate these message types:
-
surfaceUpdate: Contains your component tree
Map<String, Object> surfaceUpdate = new HashMap<>();
surfaceUpdate.put("type", "surfaceUpdate");
surfaceUpdate.put("surfaceId", "my_surface");
surfaceUpdate.put("components", componentsList);
-
dataModelUpdate: Initializes data model state
Map<String, Object> dataModelUpdate = createDataModelUpdateWithValues(
"my_surface",
Map.of("/form/name", "", "/form/email", "")
);
-
beginRendering: Optional start signal -
deleteSurface: Cleanup when done
Key A2UI Components in Java
Standard Components I'm Using
-
Text: Display text with
literalStringproperty -
TextField: Input fields bound to data model paths via
textproperty -
Button: Actions with
contextarray for data extraction - Column: Layout containers with child component IDs
Data Model Binding in Java
A2UI v0.8 uses adjacency list format. In Java, I build it like this:
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("/form/name", "");
dataModel.put("/form/email", "");
Map<String, Object> dataModelUpdate = createDataModelUpdateWithValues(
"user_form",
dataModel
);
This generates:
{
"dataModelUpdate": {
"contents": [
{
"key": "form",
"valueMap": [
{ "key": "name", "valueString": "" },
{ "key": "email", "valueString": "" }
]
}
],
"surfaceId": "user_form"
}
}
Components bind using:
-
TextField:
{ "text": { "path": "/form/name" } } - Button context: Extracts values from these paths
Button Actions in Java
Create buttons that extract data from the data model:
Map<String, String> contextBindings = new HashMap<>();
contextBindings.put("name", "/form/name");
contextBindings.put("email", "/form/email");
components.add(createButtonComponent(
"submit_button",
"Submit Form",
"processForm", // This is your @Action method name
contextBindings
));
When clicked, your action method receives the extracted values:
@Action(description = "Process form submission")
public Object processForm(String name, String email) {
// Process the form data
return createConfirmationUI(name, email);
}
Implementation Best Practices (Java)
1. Use the A2UIDisplay Interface
- Implement
A2UIDisplayin all your services - Use helper methods:
createTextComponent(),createTextFieldComponent(), etc. - Keeps code consistent and reduces duplication
2. Component ID Naming
// Good
"name_input", "submit_button", "result_text"
// Avoid
"component1", "btn", "txt"
3. Data Model Path Conventions
"/form/name" // Simple form field
"/reservation/date" // Nested resource
"/reservation/menu/entree" // Deep nesting
4. Callback Detection Pattern
@Action(description = "My action")
public Object myAction(String param) {
Object result = performBusinessLogic(param);
if(callback != null && callback.getType().equals(CallBackType.A2UI.name())) {
return createUI(result);
}
return result.toString(); // Plain text fallback
}
5. Spring Boot Best Practices
- Use
@Servicefor your agent classes - Inject dependencies with
@Autowired - Let tools4ai framework autowire
ActionCallbackandAIProcessor - Use
ThreadLocalfor callback in multi-threaded environments
Benefits of Java + A2UI
For Development
- Type Safety: Compile-time validation of A2UI message structure
- Spring Boot Ecosystem: Leverage mature enterprise features
-
Code Reusability:
A2UIDisplayinterface used across all services - Dual-Mode: Same code serves both A2UI and plain text clients
- Testable: Separate UI generation from business logic
For Deployment
- Stateless: Easy horizontal scaling
- HuggingFace Spaces: Simple deployment pipeline
- Portable: Deploy to any Java hosting environment
Live Demo
- Demo: https://vishalmysore.github.io/simplea2ui/
- Backend API: https://vishalmysore-a2ui.hf.space
Try queries like:
- "Compare Honda and Toyota for me?"
- "What food does Vishal like to eat?"
- "Can you book a restaurant for me?"
Top comments (0)