Understanding the A2UI Protocol: Building with Java and Spring Boot

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:

  1. surfaceUpdate: Contains your component tree
   Map<String, Object> surfaceUpdate = new HashMap<>();
   surfaceUpdate.put("type", "surfaceUpdate");
   surfaceUpdate.put("surfaceId", "my_surface");
   surfaceUpdate.put("components", componentsList);
  1. dataModelUpdate: Initializes data model state
   Map<String, Object> dataModelUpdate = createDataModelUpdateWithValues(
       "my_surface", 
       Map.of("/form/name", "", "/form/email", "")
   );
  1. beginRendering: Optional start signal
  2. deleteSurface: Cleanup when done

Key A2UI Components in Java

Standard Components I’m Using

  • Text: Display text with literalString property
  • TextField: Input fields bound to data model paths via text property
  • Button: Actions with context array 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 A2UIDisplay in 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 @Service for your agent classes
  • Inject dependencies with @Autowired
  • Let tools4ai framework autowire ActionCallback and AIProcessor
  • Use ThreadLocal for 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: A2UIDisplay interface 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

Try queries like:

  • “Compare Honda and Toyota for me?”
  • “What food does Vishal like to eat?”
  • “Can you book a restaurant for me?”

Similar Posts