You Already Have Server‑Driven UI. Here’s What It Still Can’t Do (And How to Validate LLM‑Generated SwiftUI Safely)
You've invested in a server‑driven UI (SDUI) system for iOS. Your client can already render dynamic user interface payloads from the backend.

You've invested in a server‑driven UI (SDUI) system for iOS. Your client can already render dynamic user interface payloads from the backend.
Add an LLM on top and it feels like you should get generative UI "for free." In practice, your existing SDUI stack still can't guarantee that LLM‑generated UI is safe, valid, or gracefully handled when it goes wrong.
This tutorial walks through how to implement client‑approved validation of server UI payloads before rendering them in SwiftUI, with:
- Schema validation for server‑driven UI on iOS
- Security and safety checks for LLM‑generated UI
- Graceful fallbacks when dynamic user interface data is invalid
We'll focus on patterns Uzori uses under the hood in its iOS SDK, but everything here applies whether you use Uzori or roll your own. If you're also thinking about AI UX for transaction disputes, see our related guide: "Where did my money go? — a transaction dispute is a four‑step status answer" (pillar article).
Why SDUI + LLM Still Needs Client‑Approved Validation
Most teams start with a simple assumption:
"If my server sends valid JSON, my SDUI renderer can just show it."
That assumption breaks once you let an LLM participate in composing the UI payload. Even with structured outputs and JSON schemas, you still need client‑approved SDUI payload validation in SwiftUI.
Structured outputs help, but they don't finish the job
Major providers now support schema‑enforced output:
- OpenAI reports 100% JSON‑schema compliance for
gpt-4o-2024-08-06vs under 40% forgpt-4-0613on their internal eval. - JSONSchemaBench evaluates 10,000 real‑world schemas across six frameworks and still finds "ample room for improvement."
- SchemaBench scales to ~40,000 schemas and concludes that even the latest models still struggle to emit fully valid JSON.
So while schema enforcement makes SDUI safer, it does not guarantee:
- The UI payload respects your navigation rules
- The layout is accessible or on‑brand
- The screen doesn't reference components the client simply doesn't have
Apple and OWASP: validate externally‑generated data on a trusted system
Apple's Secure Coding Guide and OWASP's secure coding practices emphasize three ideas that map directly to SDUI:
- Validate all external input before use
- Prefer allow‑lists over deny‑lists
- Gracefully bypass or fail closed when validation fails
In an SDUI+LLM pipeline, that means:
- Never render a SwiftUI screen directly from model output
- Always pass SDUI payloads through a server‑side validator and a client‑side gate
- Treat invalid UI as a normal case with a fallback path
Uzori bakes this in by design: the Uzori engine composes SwiftUI screens via a constrained schema, validates them on your server, and only then streams them into your app. This tutorial shows how to replicate that pattern in your own stack.
Prerequisites
Before you start, you should have:
- An existing server‑driven UI system on iOS (custom or framework‑based)
- A SwiftUI app with navigation controlled by server payloads
- A backend that can:
- Describe UI schemas (or at least JSON structures)
- Integrate with an LLM that follows those schemas
Technical prerequisites:
- Xcode 15+ and Swift 5.9+
- Familiarity with
Codableand Swift error handling - Basic understanding of OpenAPI or similar API schemas
Optional, but recommended:
- A pilot feature, such as an AI concierge flow, product exploration, or a dispute assistant like in the "Where did my money go?" pillar article.
Step 1: Define a Strong, Versioned UI Schema
The foundation of safe server‑driven UI on iOS is a typed UI schema. Your SDUI system likely already has one, but LLM participation raises the bar.
1.1 Model UI intent, not raw SwiftUI code
Instead of asking the model to emit SwiftUI, define UI intent objects:
enum ScreenType: String, Codable {
case list
case detail
case form
case wizard
}
struct UIScreenPayload: Codable {
let version: Int
let id: String
let type: ScreenType
let title: String?
let components: [UIComponent]
}
enum UIComponent: Codable {
case text(TextComponent)
case button(ButtonComponent)
case field(FieldComponent)
}
With this approach:
- The server describes what to render
- The client owns how it's rendered in SwiftUI
1.2 Introduce a schema version and component catalog
To support safe evolution:
- Add a
versionfield to top‑level payloads - Maintain a client‑approved component catalog
For example:
struct ComponentCatalog {
static let allowedTypes: Set<ScreenType> = [.list, .detail, .form, .wizard]
static let maxComponentsPerScreen = 50
}
Your LLM (or Uzori engine) should only compose screens using this catalog. Anything outside the catalog is invalid by definition.
Step 2: Validate Payloads Server‑Side Before Sending to iOS
The first line of defense for LLM‑generated UI is on the server. Even with structured outputs, you need more validation before your iOS app sees the payload.
2.1 Apply JSON schema or OpenAPI validation
Use your existing SDUI / API tooling to validate payloads:
- Validate against a JSON Schema or OpenAPI definition
- Reject any payload that doesn't conform
This is where tools like JSONSchemaBench and SchemaBench come into play. They show that even constrained decoding frameworks don't reach 100% validity across thousands of real schemas, so you must keep schema validation in your pipeline.
2.2 Add business logic and safety checks
Beyond structural validation, check:
- Permission and privacy: no component references data the user shouldn't see
- Component limits: enforce max components per screen
- Navigation safety: no links to screens the client doesn't support
In a dispute assistant inspired by "Where did my money go?" you might enforce:
- Only show dispute status types your client knows how to render
- Avoid exposing internal investigation notes that belong to back‑office users
Return only payloads that pass all checks. Log and monitor rejected payloads for debugging.
Step 3: Implement Client‑Side Schema Validation Before SwiftUI Render
Even with server‑side validation, you should re‑validate on the client. Apple and OWASP both argue for defense‑in‑depth; your renderer must never assume that remote data is safe.
3.1 Create a validation layer separate from your SwiftUI views
Introduce a small validator type:
enum UIPayloadValidationError: Error {
case unsupportedVersion
case unknownScreenType
case tooManyComponents
case missingRequiredTitle
}
struct UIPayloadValidator {
func validate(_ payload: UIScreenPayload) throws {
guard payload.version == 1 else {
throw UIPayloadValidationError.unsupportedVersion
}
guard ComponentCatalog.allowedTypes.contains(payload.type) else {
throw UIPayloadValidationError.unknownScreenType
}
guard payload.components.count <= ComponentCatalog.maxComponentsPerScreen else {
throw UIPayloadValidationError.tooManyComponents
}
if payload.type == .form && payload.title == nil {
throw UIPayloadValidationError.missingRequiredTitle
}
}
}
Key points:
- Validation happens before any SwiftUI view body runs
- Errors are explicit and typed
- This layer is independent of the LLM, Uzori, or your backend
3.2 Gate your SDUI rendering with the validator
Use the validator at the entry point of your SDUI screen:
struct DynamicScreen: View {
let payload: UIScreenPayload
@State private var isValid: Bool = true
@State private var validationError: UIPayloadValidationError?
private let validator = UIPayloadValidator()
var body: some View {
Group {
if isValid {
renderScreen(from: payload)
} else {
InvalidScreenFallback(error: validationError)
}
}
.task {
do {
try validator.validate(payload)
} catch let error as UIPayloadValidationError {
validationError = error
isValid = false
} catch {
isValid = false
}
}
}
}
Result:
- No invalid payload can reach your SwiftUI layout functions
- Every failure path is handled by a dedicated fallback view
Step 4: Build Graceful Fallbacks for Invalid UI Payloads
Validation is only half the story. When an LLM‑generated UI payload is invalid, you need graceful fallback behavior that keeps the UX coherent.
4.1 Follow Apple's "graceful bypass" pattern
Apple's App Attest guidance explicitly recommends bypassing functionality when unsupported. Apply the same idea to SDUI:
- Don't crash or show a broken layout
- Bypass the dynamic screen and show a safe, static alternative
For example:
struct InvalidScreenFallback: View {
let error: UIPayloadValidationError?
var body: some View {
VStack(spacing: 12) {
Text("We're still loading this experience.")
.font(.headline)
Text("Switching to a stable view while we fix an issue with the dynamic interface.")
.font(.subheadline)
.multilineTextAlignment(.center)
Button("Continue") {
// navigate to a known-good flow
}
}
.padding()
}
}
This preserves user trust:
- Users get a clear, honest message
- You maintain control of navigation and state
4.2 Design task‑specific fallbacks
For flows like the "Where did my money go?" dispute assistant, fallbacks should be task‑aware:
- Show a simpler four‑step status view when the generative UI fails
- Provide links to human support or FAQ
- Preserve critical information (transaction details, current dispute state)
This pattern generalizes:
- AI concierge → fallback to a standard setup wizard
- Product exploration → fallback to a filtered list view
- Dynamic configuration → fallback to your existing settings screens
Step 5: Connect Your LLM + SDUI Pipeline with Safe Contracts
At this point you have:
- A versioned UI schema
- Server‑side and client‑side validators
- Graceful fallbacks for invalid screens
Now you can safely let an LLM participate in composing UI, or adopt the Uzori SDK.
5.1 Use tools/functions that mirror your schema
When you integrate an LLM:
- Define tools / functions that match your UI schema
- Constrain the model to emit only allowed, versioned screen types
For example, your tool might be create_screen(payload: UIScreenPayload) and your server ensures any model output passes schema validation before it's returned to the client.
This aligns with emerging standards like Google's A2UI:
- Agents choose from existing component catalogs
- Clients provide validation functions
- Errors are handled explicitly, with streaming support
5.2 Or use Uzori's SDK to get the pipeline out of the box
Uzori's iOS SDK gives you:
- Generative UI, server‑driven safety: AI composes SwiftUI screens; your server validates them.
- One‑screen integration: connect your app to Uzori's engine with a single SwiftUI host screen.
- Native SwiftUI rendering: no cross‑platform runtime, no web views.
In practice, Uzori:
- Takes user intent + your OpenAPI‑described backend
- Produces generative, server UI payloads as SwiftUI screen definitions
- Validates them server‑side, then streams them into the app
You still keep client‑side validation and fallbacks, but the heavy lifting of LLM orchestration and schema enforcement lives in Uzori.
Step 6: Monitor, Test, and Iterate Safely
Validation isn't static. Once your SDUI+LLM pipeline is live, you need to observe, test, and refine.
6.1 Log invalid payloads and validation errors
Capture:
- Counts of payloads rejected server‑side
- Counts of payloads rejected client‑side
- The specific
UIPayloadValidationErrorvalues
This helps you:
- Tune your schema and component catalog
- Spot mis‑configurations in your LLM prompts and tools
- Identify tasks that need more explicit design (e.g., dispute flows)
6.2 Test with synthetic and real workloads
Use:
- Synthetic payloads that intentionally break rules
- Real traffic from beta testers
Goal:
- Ensure your graceful fallback paths are exercised
- Confirm your app never crashes or renders unsafe UI
Over time, you can safely expand:
- New screen types
- More complex flows (wizards, product comparisons, concierge journeys)
- Higher‑stakes experiences like "Where did my money go?" dispute assistants
Frequently Asked Questions
What does "client‑approved SDUI payload" mean in SwiftUI?
A client‑approved SDUI payload is server UI data that has passed both server‑side and client‑side validation before being rendered as SwiftUI. The iOS app checks schema version, screen type, component counts, and any app‑specific rules, and only then maps the payload to native views.
Why should I validate server UI payloads before rendering SwiftUI?
Validating server UI payloads prevents:
- Crashes from malformed JSON or unexpected screen types
- Security issues from unvalidated external input
- UX regressions from oversized or unsupported layouts
Apple and OWASP treat unvalidated input as a core risk. LLM‑generated UI makes this risk more likely, so validation is mandatory.
Isn't LLM structured output enough for safe server‑driven UI on iOS?
No. Structured output improves syntactic correctness, but research like JSONSchemaBench (10K schemas) and SchemaBench (~40K schemas) shows models still struggle. More importantly, structured output doesn't enforce business rules, permissions, navigation contracts, or component catalogs — your validators must.
How does Uzori's SDK handle validation differently from my existing SDUI system?
Uzori combines generative UI with server‑driven safety:
- AI composes SwiftUI screens using a constrained schema
- Your server validates every screen before streaming it to the client
- The iOS SDK renders only validated, native SwiftUI payloads
If you already have SDUI, Uzori gives you an AI interface layer that respects your architecture while adding dynamic, task‑specific flows.
What should my app do when the dynamic UI payload is invalid?
Follow Apple's "graceful bypass" pattern:
- Don't render the invalid payload
- Show a fallback screen that keeps the user moving
- Prefer task‑specific alternatives (e.g., a four‑step dispute status view for a "Where did my money go?" flow)
This keeps your app stable and trustworthy while you iterate on generative UI.
By implementing client‑approved validation of server UI payloads before rendering SwiftUI, and designing graceful fallbacks for invalid data, you turn your existing SDUI system into a safe, AI‑native UI runtime. Whether you build your own pipeline or plug in Uzori's SDK, the pattern is the same:
Validate first, render later.
That's what unlocks AI interfaces that feel like your app, not just another chat box pasted on top.