Live demo — this example is server-driven; run the showcase locally for the interactive version.

JSON view models

Every pha model deserializes from plain JSON — the view-model contract. A service (or a View Descriptor Protocol descriptor) names a template, hands over a JSON payload, and the server renders PatternFly HTML: no Java builder code in the request path, no HTML assembled in the browser.

Quick start

Bring Jackson yourself (io.quarkus:quarkus-jackson in a Quarkus app) — pha ships only the annotations, so applications that never touch JSON pay nothing. Then bind and render:

String payload = """
    {"id": "mn-api", "items": [{"text": "Edit"}, {"text": "Delete", "danger": true}]}
    """;
Menu menu = objectMapper.readValue(payload, Menu.class);
String html = engine.getTemplate("pha:components/navigation/menu").data("menu", menu).render();

Validation carried by the builders still applies on the JSON path — an empty menu, a badge without a value, or a payload that does not bind is rejected at readValue time, never half-rendered.

Shape rules

JSON property names are the model's field names — the same names the Java builders use, so the Java tab on any example doubles as the property reference. The rules, by model shape:

  • Builder models (Menu, Table, Card, …): properties bind to the builder — booleans for the toggles ({"plain": true}), arrays for item lists ({"items": [...]}), nested objects for composed models. Java-only conveniences that set several fields at once (searchFilter(a, b)) are covered by their field-level properties (searchPlaceholder, searchAriaLabel).
  • Immutable item models (MenuItem, TabItem, Label, …): the fields bind directly — {"text": "Delete", "danger": true} is the JSON form of MenuItem.of("Delete").asDanger().
  • Internal state is not contract: computed wiring (Alpine state, resolved tree rows) has no JSON property. The contract is enforced by a sweep test over all 100 models — a model that gains a field without a JSON binding fails the build.

The pha: template URI

pha:components/navigation/menu is a stable, descriptor-friendly address for a Qute template. The scheme resolves from any classpath templates/ root — pha's own components, your application's fragments, and template jars published by API services — so a descriptor can name any fragment without knowing which artifact ships it. Traversal outside templates/ does not resolve.

Try it

This showcase exposes the flow as an endpoint (POST /api/pha/render?template=…&model=… — showcase-only, not part of the extension). Edit the payload and render: the HTML below comes entirely from the server.