* autoFixSpec prunes children references to undefined elements
Dangling references are the dominant remaining first-attempt validation
failure in benchmarks, and models frequently fail to repair them even given
the exact error (observed: three repair turns, same dangling footer each
time). The renderer already skips missing children at runtime, so pruning
yields the identical rendered output while letting the spec validate. Each
removal is reported in fixes.
* Classify autoFixSpec fixes as lossy or lossless
Pruning a dangling child reference changes what renders; relocating a
misplaced field does not. Callers with a repair loop need to tell these
apart: accept lossless fixes silently, prefer re-prompting over lossy fixes,
and keep the lossy-fixed spec as a last resort. Adds fixDetails alongside the
existing fixes strings (additive, no signature break).
* Validate visible conditions in validateSpec; document filtered-list pattern
Malformed visible conditions (e.g. mixing $state and $item in one object)
silently evaluate to hidden at runtime: evaluateCondition dispatches on the
first recognized key and non-strict parsing strips the rest, so whole regions
of UI disappear with a valid-looking spec. Benchmarked worst case: a kanban
board that rendered zero task cards.
- core: VisibilityConditionStrictSchema (strict objects, exported)
- core: validateSpec rejects malformed visible with a repairable message
listing the valid forms (code: invalid_visible)
- framework prompts: FILTERED LISTS rule showing the repeat + per-item
visible pattern models keep reaching for and inventing syntax around
* Support filtered lists: repeat + $item visible on the same element
Models across vendors consistently write {repeat, visible: {$item: ...}} on
one container to mean a filtered list (kanban columns, status sections).
Previously $item had no meaning outside the repeat scope, the condition
evaluated false, and the whole region silently disappeared — the worst
visual failures in benchmarks were boards rendering zero cards this way.
Outside a repeat scope that spelling was always broken, so claiming it is
backward compatible: the renderer now applies such a condition per item,
preserving original indices for item state paths. Container-level $state
conditions and per-child $item conditions behave as before.
- core: conditionUsesItemScope helper (exported)
- react: RepeatChildren filters items by the container's $item condition
- framework prompts: FILTERED LISTS rule teaches the container spelling
- react: repeat-filter test suite (filtering, no-filter, $state container
visibility, per-child $item)
Other framework renderers (vue, svelte, solid, react-native) still evaluate
the container condition outside scope and should adopt the same semantics.
* Validate repeat containers: require children and matching state arrays
Two silent empty-region failures seen repeatedly in benchmarks, both passing
validation today: a repeat element with no children (nothing to clone per
item) and a repeat statePath pointing at a missing or non-array state value.
Both now fail validateSpec with repairable messages (repeat_without_children,
repeat_state_mismatch). State checks only run when the spec provides state;
runtime-fed state is unaffected.
* Address review: lossy-aware hook repair, react-only filtered-list rule, reuse getByPath
- ink/react-native useUIStream repair loops no longer accept lossy autofixes
unconditionally: lossless relocations apply immediately, pruned content
holds back while retries remain (so validation fails and the model repairs
the missing elements) and applies only as a last resort.
- FILTERED LISTS prompt rule removed from schemas whose renderers do not
implement the per-item filter yet (everything except react). Renderer
parity tracked as follow-up.
- repeat_state_mismatch validation reuses getByPath instead of a local JSON
Pointer lookup that skipped ~0/~1 unescaping.
* Address review: split mixed repeat visibility, apply lossless fixes eagerly
- splitRepeatVisibility (core, exported): AND-composed conditions on a repeat
container partition into a container gate ($state conjuncts, hides the
shell) and a per-item filter ($item/$index conjuncts). Mixed $or cannot
partition soundly and stays fully per-item, documented.
- react renderer uses the split, so {$and: [{$state gate}, {$item filter}]}
hides the empty shell when the gate is false instead of rendering a husk.
- autoFixSpec gains { lossy?: boolean } (default true, additive): ink and
react-native repair loops now apply lossless relocations immediately and
withhold only the pruning until retries are exhausted, matching the stated
intent.
* Address review: RN final-validation error path, repeat prune guard, docs
- react-native useUIStream mirrors ink: when retries are exhausted and the
spec still fails validation, report through onError instead of silently
calling onComplete with an invalid spec.
- autoFixSpec never prunes a repeat container to zero children; that would
trade missing_child for repeat_without_children and leave the last-resort
spec unrenderable. The dangling template reference stays visible to repair.
- Docs for the new surface: core README + skill (validateSpec issue codes,
fixDetails, lossy option), react README + skill and web visibility docs
(filtered-list pattern, mixed-condition splitting, framework support note).
@json-render/react-native
React Native renderer for json-render. Turn JSON specs into native mobile UIs with standard components, data binding, visibility, actions, and dynamic props.
Installation
npm install @json-render/react-native @json-render/core zod
Quick Start
1. Create a Catalog
import { defineCatalog } from "@json-render/core";
import { schema } from "@json-render/react-native/schema";
import {
standardComponentDefinitions,
standardActionDefinitions,
} from "@json-render/react-native/catalog";
import { z } from "zod";
export const catalog = defineCatalog(schema, {
components: {
...standardComponentDefinitions,
// Add custom components
Icon: {
props: z.object({
name: z.string(),
size: z.number().nullable(),
color: z.string().nullable(),
}),
slots: [],
description: "Icon display using Ionicons",
},
},
actions: standardActionDefinitions,
});
2. Define Custom Component Implementations
import { defineRegistry, type Components } from "@json-render/react-native";
import Ionicons from "@expo/vector-icons/Ionicons";
import { catalog } from "./catalog";
export const { registry } = defineRegistry(catalog, {
components: {
Icon: ({ props }) => (
<Ionicons
name={props.name as keyof typeof Ionicons.glyphMap}
size={props.size ?? 24}
color={props.color ?? "#111827"}
/>
),
} as Components<typeof catalog>,
});
Standard components (Container, Row, Column, Button, TextInput, etc.) are included by default. You only need to register custom ones.
3. Render Specs
import {
Renderer,
StateProvider,
VisibilityProvider,
ActionProvider,
ValidationProvider,
} from "@json-render/react-native";
import { registry } from "./registry";
function App({ spec }) {
return (
<StateProvider initialState={{}}>
<VisibilityProvider>
<ActionProvider handlers={{}}>
<ValidationProvider>
<Renderer spec={spec} registry={registry} />
</ValidationProvider>
</ActionProvider>
</VisibilityProvider>
</StateProvider>
);
}
Standard Components
Layout
| Component | Description |
|---|---|
Container |
Basic wrapper with padding, background, border radius |
Row |
Horizontal flex layout with gap, alignment, flex |
Column |
Vertical flex layout with gap, alignment, flex |
ScrollContainer |
Scrollable area (vertical or horizontal) |
SafeArea |
Safe area insets for notch/home indicator |
Pressable |
Touchable wrapper that triggers actions on press |
Spacer |
Fixed or flexible spacing between elements |
Divider |
Thin line separator |
Content
| Component | Description |
|---|---|
Heading |
Heading text (levels 1-6) |
Paragraph |
Body text |
Label |
Small label text |
Image |
Image display with sizing modes |
Avatar |
Circular avatar image |
Badge |
Small status badge |
Chip |
Tag/chip for categories |
Input
| Component | Description |
|---|---|
Button |
Pressable button with variants |
TextInput |
Text input field |
Switch |
Toggle switch |
Checkbox |
Checkbox with label |
Slider |
Range slider |
SearchBar |
Search input |
Feedback
| Component | Description |
|---|---|
Spinner |
Loading indicator |
ProgressBar |
Progress indicator |
Composite
| Component | Description |
|---|---|
Card |
Card container with optional header |
ListItem |
List row with title, subtitle, accessory |
Modal |
Bottom sheet modal |
Visibility Conditions
Elements can use visible to show/hide based on state. Same syntax as @json-render/react: { "$state": "/path" }, { "$state": "/path", "eq": value }, { "$state": "/path", "not": true }, or [ cond1, cond2 ] for AND.
Pressable Component
The Pressable component wraps children and triggers an action on press. It's essential for building interactive UIs like tab bars:
{
"type": "Pressable",
"props": {
"action": "setState",
"actionParams": { "statePath": "/activeTab", "value": "home" }
},
"children": ["home-tab-icon", "home-tab-label"]
}
Built-in Actions
The setState action is handled automatically by ActionProvider. It updates the state model, which triggers re-evaluation of visibility conditions and dynamic prop expressions:
{
"action": "setState",
"actionParams": { "statePath": "/activeTab", "value": "home" }
}
Dynamic Prop Expressions
Any prop value can be a dynamic expression resolved at render time:
{
"type": "Icon",
"props": {
"name": {
"$cond": { "$state": "/activeTab", "eq": "home" },
"$then": "home",
"$else": "home-outline"
},
"color": {
"$cond": { "$state": "/activeTab", "eq": "home" },
"$then": "#007AFF",
"$else": "#8E8E93"
}
}
}
See @json-render/core for full expression syntax.
Tab Navigation Pattern
Combine Pressable, setState, visibility conditions, and dynamic props for functional tabs:
- Each tab button is a
Pressablewithaction: "setState"andactionParams: { statePath: "/activeTab", value: "tabName" } - Tab icons/labels use
$conddynamic props for active/inactive styling - Tab content sections use
visibleconditions:{ "$state": "/activeTab", "eq": "tabName" }
AI Prompt Generation
const systemPrompt = catalog.prompt({
customRules: [
"Use SafeArea as the root element",
"Use Pressable + setState for interactive tabs",
],
});
External Store (Controlled Mode)
For full control over state, pass a StateStore to bypass the internal state and wire json-render to any state management library (Redux, Zustand, XState, etc.):
import { createStateStore, type StateStore } from "@json-render/react-native";
// Use the built-in store outside of React
const store = createStateStore({ count: 0 });
<StateProvider store={store}>
{children}
</StateProvider>
// Mutate from anywhere — React will re-render automatically:
store.set("/count", 1);
When store is provided, initialState and onStateChange are ignored. The store is the single source of truth. The same store prop is available on createRenderer, JSONUIProvider, and StateProvider.
Hooks
| Hook | Purpose |
|---|---|
useStateStore() |
Access state context (state, get, set, update) |
useStateValue(path) |
Get single value from state |
useStateBinding(path) |
Two-way data binding (returns [value, setValue]) |
useVisibility() |
Access visibility evaluation |
useIsVisible(condition) |
Check if condition is met |
useActions() |
Access action context |
useAction(name) |
Get a single action dispatch function |
useUIStream(options) |
Stream specs from an API endpoint |
createStandardActionHandlers(options) |
Create handlers for standard actions |
createStateStore(initialState) |
Create a framework-agnostic in-memory StateStore |