We use cookies to improve your experience.

Mobile Reality logoMobile Reality logo

Structured LLM Output Without JSON Schemas, A Different Approach

Breaking JSON brackets symbolizing structured LLM output with extended Markdown forms and tables for improved reasoning accuracy

Introduction

Every developer integrating LLMs hits the same wall: the model returns plain text, and you need structured data. The industry converged on JSON Schema, Pydantic models, and function calling to solve this. But what if forcing JSON on LLMs is the wrong abstraction entirely?

This article introduces a different paradigm, one where structured output is not just machine-parseable, but also human-renderable. Where the LLM doesn't return a JSON blob you have to build UI for, but a document that is the UI.

The JSON Schema Era: How We Got Here

From Regex Parsing to Pydantic, A Brief History

In 2022, getting structured data from an LLM meant prompt engineering and prayer. You'd write "respond in JSON format" and hope for the best. When the model inevitably returned Sure! Here's the JSON: before the actual payload, you'd write regex to extract it.

Then came the tooling wave:

  • Instructor (2023) wrapped Pydantic models around LLM calls, giving you type-safe extraction with automatic retries when validation failed.
  • Outlines and Guidance (2023-2024) took it further with constrained decoding, forcing the model's token generation to follow a grammar, guaranteeing valid JSON at the token level.
  • OpenAI Structured Outputs (2024) made it a first-class API feature. Define a JSON Schema, get conforming output. No parsing, no retries.

By 2025, the ecosystem settled: if you want structured data from an LLM, you define a schema and the model fills it in. Problem solved.

Or is it?

Why Every LLM Provider Shipped JSON Mode

The answer is simple: developers asked for it. When you're building a pipeline, LLM output feeds into a database, triggers a function, or populates a UI, you need predictable structure. JSON is universal. Every language parses it. Every database stores it.

But this framing treats LLM output as data transfer , a serialization problem between the model and your backend. That's one valid use case. It's not the only one.

The Hidden Costs of Forcing JSON on LLMs

34% More Tokens, The JSON Tax You're Already Paying

JSON is verbose by design. Curly braces, quoted keys, colons, commas, all structural tokens that carry no semantic content. Research and benchmarks consistently show that Markdown is 34-38% more token-efficient than JSON for equivalent data.

Here's the same information in both formats:

JSON (87 tokens):

json
{

  "patient": {

    "name": "Jane Doe",

    "date_of_birth": "1990-03-15",

    "chief_complaint": "Persistent headache for 3 days",

    "vitals": {

      "blood_pressure": "120/80",

      "heart_rate": 72,

      "temperature": 98.6

    },

    "assessment": "Tension-type headache. No red flags.",

    "plan": "OTC analgesics, follow up in 1 week if no improvement"

  }

}

Markdown with MDMA (fewer tokens, and the user can actually read it):

javascript
# Patient Intake



id: patient-intake

type: form

fields:

  • name: patient_name

type: text

label: Patient Name

required: true

sensitive: true

  • name: dob

type: date

label: Date of Birth

sensitive: true

  • name: chief_complaint

type: textarea

label: Chief Complaint

required: true

  • name: assessment

type: textarea

label: Assessment

  • name: plan

type: textarea

label: Plan

onSubmit: submit-intake

`

javascript


The JSON version gives you data. The [MDMA](https://github.com/mobile-reality/mdma) version gives you data **and** an interactive form the user fills out, without building a single line of frontend code.



At scale, the token difference compounds. If your application makes 10,000 LLM calls per day, switching from JSON to Markdown-based structured output can cut your token costs by a third.



### 10-15% Reasoning Degradation in JSON Mode



This one is less discussed but well-documented. When you force an LLM into JSON mode, you're constraining its generation process. The model can no longer "think out loud", it must produce valid JSON from the first token.



Multiple benchmarks show **10-15% performance degradation on reasoning tasks** when using constrained JSON output compared to free-form generation. The model is spending capacity on structural compliance instead of problem-solving.



With Markdown, the LLM writes in the format it was trained on. Markdown is the lingua franca of the internet, it appears massively in training data. The model doesn't fight the format; it flows with it.



### Schema Boilerplate That Scales with Complexity



A simple JSON Schema for a contact form is manageable. A schema for a multi-step KYC workflow with conditional fields, nested objects, and validation rules? You're looking at hundreds of lines of schema definition before writing any application logic.



Here's what a KYC form schema looks like in Pydantic:



class Address(BaseModel):

street: str

city: str

state: str

zip_code: str = Field(pattern=r'^\d{5}(-\d{4})?$')

class KYCApplication(BaseModel):

fullname: str = Field(minlength=2, max_length=100)

dateofbirth: date

ssn: str = Field(pattern=r'^\d{3}-\d{2}-\d{4}$')

email: EmailStr

phone: str

address: Address

employment_status: Literal['employed', 'self-employed', 'unemployed', 'retired']

annual_income: float = Field(ge=0)

sourceoffunds: str

is_pep: bool # Politically Exposed Person

documents_provided: list[str]

javascript


Then you need a separate UI component to render this form, validation logic in the frontend, and mapping between the schema and the rendered fields.



With MDMA, the schema, the UI definition, and the validation rules are one thing:



mdma
id: kyc-form

type: form

fields:

  - name: full_name

    type: text

    label: Full Legal Name

    required: true

    sensitive: true

  - name: date_of_birth

    type: date

    label: Date of Birth

    required: true

    sensitive: true

  - name: ssn

    type: text

    label: Social Security Number

    required: true

    sensitive: true

    validation:

      pattern: "^\\d{3}-\\d{2}-\\d{4}$"

      message: "Format: XXX-XX-XXXX"

  - name: email

    type: email

    label: Email Address

    required: true

    sensitive: true

  - name: employment_status

    type: select

    label: Employment Status

    required: true

    options:

      - { label: Employed, value: employed }

      - { label: Self-Employed, value: self-employed }

      - { label: Unemployed, value: unemployed }

      - { label: Retired, value: retired }

  - name: annual_income

    type: number

    label: Annual Income

    required: true

    validation:

      min: 0

  - name: source_of_funds

    type: textarea

    label: Source of Funds

    required: true

  - name: is_pep

    type: checkbox

    label: Politically Exposed Person (PEP)

onSubmit: submit-kyc

`` `

One definition. The LLM generates it. The renderer displays it. The runtime validates and collects the data. No Pydantic model. No separate frontend component. No mapping layer.

Function Calling vs Structured Output vs Generative UI, When to Use Which

The industry treats these as competing approaches. They're not, they solve different problems.

Function Calling: Great for Actions, Wrong for Data Collection

Function calling (tool use) excels when you want the LLM to do something: search a database, call an API, send a message. The model decides which function to invoke and with what parameters.

But function calling is awkward for collecting data from users. The model calls a collectuserinfo function, your backend receives JSON, and then... you still need to build UI to display and edit that data. Function calling is a model-to-system interface, not a model-to-user interface.

JSON Structured Output: Reliable but Invisible to Users

Structured output via JSON Schema gives you validated, typed data. It's the right choice when the output feeds directly into a pipeline, no human in the loop.

But the moment a human needs to see, review, or modify that output, you're building custom UI. Every new schema means a new form component. Every schema change means a frontend update.

Generative UI: The Missing Third Option

What if the LLM's output was the interface?

This is the generative UI paradigm: instead of the model returning data that you render, the model returns a renderable document that collects, displays, and processes data.

MDMA implements this with extended Markdown. The LLM writes standard Markdown (headings, paragraphs, lists) interspersed with YAML-defined interactive components. A single renderer handles every document. No per-schema UI work.

Use function calling when the LLM needs to take action on behalf of the user. Use JSON structured output when the data flows into a machine-only pipeline. Use generative UI (MDMA) when a human needs to interact with the output.

What If the Structured Output Was Also the UI?

The False Choice Between Machine-Parseable and Human-Readable

The current paradigm forces a choice: either the LLM returns data (JSON, machine-parseable, invisible to users) or text (Markdown, human-readable, unstructured). You pick one and build infrastructure for the other.

MDMA rejects this trade-off. An MDMA document is:

  • Human-readable: it's Markdown. Headings, paragraphs, and lists render as expected.
  • Machine-parseable: mdma blocks parse into typed AST nodes with known schemas.
  • Interactive: forms collect input, buttons trigger actions, approval gates enforce workflows.
  • Auditable: every interaction is logged with tamper-evident hash chaining.

The same document serves the user, the system, and compliance, without conversion layers.

Markdown as a Structured Format, Why LLMs Already Prefer It

LLMs don't "prefer" JSON. They were trained on the internet, and the internet runs on Markdown. README files, documentation, forum posts, chat messages, all Markdown or Markdown-adjacent.

When you ask an LLM to generate Markdown, you're asking it to work in its native medium. When you ask it to generate JSON, you're asking it to switch to a format optimized for machines, not for language models.

MDMA leans into this. The LLM writes Markdown as usual, and when it needs to express structure, a form, a table, a workflow gate, it drops into a mdma YAML block. The transition is natural:

javascript
Based on your description, this sounds like a P2 incident.

Let me set up the triage workflow.



## Incident Assessment



id: incident-form

type: form

fields:

  • name: incident_title

type: text

label: Incident Title

required: true

  • name: severity

type: select

label: Severity Level

required: true

options:

  • { label: "P1 - Critical", value: P1 }
  • { label: "P2 - High", value: P2 }
  • { label: "P3 - Medium", value: P3 }
  • { label: "P4 - Low", value: P4 }
  • name: affected_systems

type: text

label: Affected Systems

required: true

  • name: customer_impact

type: textarea

label: Customer Impact

required: true

onSubmit: submit-incident

`

Once submitted, I'll route this to the on-call team and

create the response checklist.

jsx


Natural language and structured components coexist in one document. The model doesn't need to choose between explaining context and collecting data, it does both.



## Extended Markdown: Forms, Tables, and Approval Gates from LLM Output



### How MDMA Extends Markdown with Interactive Components



[MDMA (Markdown Document with Mounted Applications)](https://github.com/mobile-reality/mdma) adds nine component types to standard Markdown via fenced code blocks with the `mdma` language tag:



| Component | Purpose | Example Use Case |

| --- | --- | --- |

| <code>form</code> | Structured data collection | Patient intake, bug reports, KYC |

| <code>table</code> | Tabular data with sorting/filtering | Search results, audit logs |

| <code>approval-gate</code> | Multi-step approval workflow | Manager sign-off, compliance review |

| <code>tasklist</code> | Checklist with completion tracking | Pre-deploy checklist, triage steps |

| <code>button</code> | Clickable action with confirmation | Notify Slack, deploy to production |

| <code>callout</code> | Highlighted message (info/warning/error) | SLA warnings, compliance notices |

| <code>chart</code> | Data visualization | Revenue trends, error rates |

| <code>webhook</code> | HTTP request trigger | Slack notifications, API calls |

| <code>thinking</code> | Collapsed AI reasoning | Debug transparency |



Components communicate through bindings, `{{field_name}}` expressions that resolve at runtime. When a user fills a form field, bound components update automatically.



### Code Example: From JSON Schema to a Single Prompt



**The JSON Schema approach** requires three artifacts:



1. Schema definition (Pydantic/JSON Schema)

2. LLM integration (function calling or structured output)

3. Frontend component (React form mapped to schema fields)



**The MDMA approach** requires one:



import { buildSystemPrompt } from '@mobile-reality/mdma-prompt-pack';

const systemPrompt = buildSystemPrompt({

customPrompt: `You are a customer support assistant. When a user

reports an issue, generate an MDMA form to collect structured

details, followed by a tasklist for resolution steps.`,

});

// Send to any LLM, OpenAI, Claude, Gemini, local models

const response = await llm.chat({

system: systemPrompt,

messages: conversation,

});

// The response contains Markdown with ```mdma blocks

// Parse it into an interactive document

import { unified } from 'unified';

import remarkParse from 'remark-parse';

import { remarkMdma } from '@mobile-reality/mdma-parser';

const processor = unified().use(remarkParse).use(remarkMdma);

const ast = await processor.run(processor.parse(response));

javascript


The LLM decides what fields to include based on the conversation. No predefined schema. The form structure emerges from context.



### What the User Actually Sees vs What the System Parses



When the LLM generates this response:



I've reviewed your request. Here's the change management form:

mdma
id: change-request

type: form

fields:

  - name: change_title

    type: text

    label: Change Title

    required: true

  - name: risk_level

    type: select

    label: Risk Level

    options:

      - { label: Low, value: low }

      - { label: Medium, value: medium }

      - { label: High, value: high }

  - name: rollback_plan

    type: textarea

    label: Rollback Plan

    required: true

onSubmit: submit-change

`` `



id: tech-lead-approval

type: approval-gate

title: Tech Lead Approval

requiredApprovers: 1

allowedRoles:

  • tech-lead
  • engineering-manager

onApprove: proceed-to-deploy

onDeny: return-to-author

requireReason: true

`

jsx


**The user sees**: a paragraph of text, a rendered form with input fields and dropdowns, and an approval gate with Approve/Deny buttons.



**The system sees**: typed AST nodes, validated component schemas, dispatchable actions, and a binding map connecting the form to the approval gate.



**The audit log sees**: every field change, every button click, every approval decision, timestamped, actor-tagged, and hash-chained.



One response. Three consumers. Zero custom UI code.



## MDMA vs Instructor vs Outlines vs Guidance



### Feature Comparison at a Glance



| Feature | Instructor | Outlines | Guidance | MDMA |

| --- | --- | --- | --- | --- |

| <strong>Output format</strong> | JSON (Pydantic) | JSON/regex/grammar | JSON/structured text | Markdown + YAML components |

| <strong>Schema definition</strong> | Python classes | JSON Schema/regex | Template programs | YAML in Markdown |

| <strong>Requires custom UI</strong> | Yes | Yes | Yes | No, renderer included |

| <strong>Human-readable output</strong> | No (raw JSON) | No | Partially | Yes, it's Markdown |

| <strong>Approval workflows</strong> | Not built-in | Not built-in | Not built-in | First-class&nbsp;<code>approval-gate</code> |

| <strong>Audit trail</strong> | Not built-in | Not built-in | Not built-in | Hash-chained event log |

| <strong>PII handling</strong> | Not built-in | Not built-in | Not built-in | Automatic detection &amp; redaction |

| <strong>Token efficiency</strong> | JSON overhead | JSON overhead | Varies | 34-38% fewer tokens |

| <strong>LLM compatibility</strong> | OpenAI, Anthropic, others | Local models (HF) | Local models | Any LLM (prompt-based) |

| <strong>Validation</strong> | Pydantic validators | Grammar constraints | Template constraints | YAML schema + field validation |



### When You Still Need JSON (and When You Don't)



**Use Instructor/Outlines/Guidance when:**



- Output feeds directly into a machine pipeline (no human interaction)

- You need guaranteed JSON for database insertion or API calls

- You're doing bulk extraction from documents (NER, classification)

- Latency is critical and you need constrained decoding



**Use MDMA when:**



- A human needs to see, review, or interact with the output

- You need approval workflows or multi-step processes

- Compliance requires audit trails (HIPAA, SOX, MiFID)

- You want the LLM to dynamically decide what data to collect

- You're building a chatbot that needs to do more than return text



The key distinction: Instructor and friends solve **LLM  ->  machine** communication. MDMA solves **LLM  ->  human  ->  machine** communication.



## Building Human-in-the-Loop Workflows Without a Separate Frontend



### Approval Gates as First-Class Output



Most human-in-the-loop implementations follow this pattern:



1. LLM produces JSON

2. Backend stores the JSON

3. Frontend renders a custom approval UI

4. User approves/denies

5. Backend processes the decision



That's five steps across three systems. With MDMA, it's one:



The deployment package is ready for review.

mdma
id: deploy-approval

type: approval-gate

title: Production Deployment Approval

description: >

  Deploying v2.4.1 to production. Changes include

  the new payment processing module and updated rate limits.

requiredApprovers: 2

allowedRoles:

  - tech-lead

  - engineering-manager

  - director

onApprove: execute-deployment

onDeny: return-to-author

requireReason: true

`` `

The LLM generates the approval gate. The renderer displays it. The runtime enforces role-based access, collects approvals, and fires the appropriate action. The event log records who approved, when, and why, with hash-chained integrity.

No custom UI. No separate approval system. No integration work.

Multi-Step Forms Driven by Conversation Context

MDMA components communicate through bindings, enabling multi-step workflows within a single document:

javascript
## Step 1: Risk Assessment



id: risk-form

type: form

fields:

  • name: change_type

type: select

label: Change Type

required: true

options:

  • { label: Infrastructure, value: infrastructure }
  • { label: Application, value: application }
  • { label: Database, value: database }
  • name: risk_level

type: select

label: Risk Level

required: true

options:

  • { label: Low, value: low }
  • { label: Medium, value: medium }
  • { label: High, value: high }

onSubmit: assess-risk

`

Step 2: Pre-deployment Checklist

mdma
id: pre-deploy-checklist

type: tasklist

items:

  - id: tests-pass

    text: All tests pass in CI

    required: true

  - id: code-review

    text: Code reviewed by 2 engineers

    required: true

  - id: staging-verified

    text: Changes verified on staging

    required: true

  - id: rollback-ready

    text: Rollback plan documented

    required: true

onComplete: checklist-done

Step 3: Approval

mdma
id: change-approval

type: approval-gate

title: Change Approval

description: >

  Approve deployment of {{change_type}} change

  with {{risk_level}} risk level.

requiredApprovers: 1

allowedRoles:

  - tech-lead

  - engineering-manager

onApprove: execute-change

onDeny: reject-change

requireReason: true

`` `

Notice: {{changetype}} and {{risklevel}} in the approval gate description are bindings to the form fields above. When the user fills the form, the approval gate description updates in real time.

The entire workflow, assessment, checklist, approval, lives in one document generated by one LLM response. No orchestration service. No state machine definition. No workflow engine configuration.

Getting Started with MDMA

Installation and First Render

javascript
npm install @mobile-reality/mdma-parser \

            @mobile-reality/mdma-runtime \

            @mobile-reality/mdma-renderer-react \

            @mobile-reality/mdma-prompt-pack

Parse and render an MDMA document in under 20 lines:

typescript
import { unified } from 'unified';

import remarkParse from 'remark-parse';

import { remarkMdma } from '@mobile-reality/mdma-parser';

import { createDocumentStore } from '@mobile-reality/mdma-runtime';

import { MdmaProvider, MdmaDocument } from '@mobile-reality/mdma-renderer-react';



// Parse

const processor = unified().use(remarkParse).use(remarkMdma);

const ast = await processor.run(processor.parse(markdownString));



// Create runtime

const store = createDocumentStore(ast, {

  documentId: 'my-doc',

  sessionId: crypto.randomUUID(),

  environment: 'production',

});



// Render

function App() {

  return (

    <MdmaProvider store={store} ast={ast}>

      <MdmaDocument />

    </MdmaProvider>

  );

}

Plugging into Your Existing LLM Pipeline

MDMA works with any LLM. Use the prompt pack to give the model MDMA authoring capabilities:

typescript
import { buildSystemPrompt } from '@mobile-reality/mdma-prompt-pack';



const systemPrompt = buildSystemPrompt({

  customPrompt: `You are an HR onboarding assistant. When a new

  employee needs to be onboarded, generate MDMA forms to collect

  their information, a tasklist for IT setup, and an approval

  gate for their manager.`,

});



// Works with OpenAI

const response = await openai.chat.completions.create({

  model: 'gpt-4o',

  messages: [

    { role: 'system', content: systemPrompt },

    { role: 'user', content: 'New hire: Senior Engineer starting March 1st' },

  ],

});



// Works with Anthropic

const response = await anthropic.messages.create({

  model: 'claude-sonnet-4-20250514',

  system: systemPrompt,

  messages: [

    { role: 'user', content: 'New hire: Senior Engineer starting March 1st' },

  ],

});



// Parse the response, same code regardless of LLM provider

const ast = await processor.run(processor.parse(response.content));

const store = createDocumentStore(ast, { /* config */ });

The LLM generates the document structure dynamically based on conversation context. Different conversations produce different forms, different checklists, different approval flows, all handled by the same renderer.

Conclusion

JSON Schema solved the structured LLM output problem for machine-to-machine communication. But the moment a human enters the loop, reviewing data, filling forms, approving decisions, JSON becomes the wrong abstraction. You end up building custom UI for every schema, managing state across frontend and backend, and bolting on audit trails as an afterthought.

MDMA takes a different approach: let the LLM write what it's best at (Markdown), extend it with interactive components (YAML blocks), and handle everything else, rendering, validation, state management, audit logging, in the runtime.

The result: structured output that humans can actually use, without the JSON tax.

When the structure has to come from a model you own rather than a hosted API, that becomes a training and deployment problem instead of a prompting one. We cover that in our custom LLM development and fine-tuning work.

Links:

AI-Powered Interactive Documents & Generative UI Insights

Are you exploring how large language models can move beyond plain text to deliver structured, interactive experiences? At MDMA, we're pioneering the intersection of Markdown and generative UI — enabling LLMs to return forms, approval workflows, and dynamic components instead of static responses. Our growing library of articles covers the technical foundations, business applications, and architectural patterns behind this shift:

Dive into these resources to understand why generative UI is replacing plain-text chat interfaces across healthcare, fintech, and enterprise workflows. If you'd like to integrate MDMA into your product or explore a partnership, reach out to our team. And if you're passionate about shaping the future of LLM-powered interfaces, check our open positions — we're hiring.

Did you like the article?Find out how we can help you.

Matt Sadowski

CEO of Mobile Reality

CEO of Mobile Reality

Related articles

Forward deployed engineering puts engineers inside your systems, not on a call. The role, the skills, the economics, and when to buy it.

06.08.2026

Forward Deployed Engineering: The Delivery Model AI Made Necessary

Forward deployed engineering puts engineers inside your systems, not on a call. The role, the skills, the economics, and when to buy it.

Read full article

Learn how generative UI builds personalized, reliable interfaces from trusted components with 1,377 tests revealing best formats for robust production use.

28.07.2026

Generative UI: How AI Builds User Interfaces on Demand

Learn how generative UI builds personalized, reliable interfaces from trusted components with 1,377 tests revealing best formats for robust production use.

Read full article

Explore how AG-UI protocol streams 17 event types for real-time AI agent interaction, contrasting with MDMA's Markdown-based generative UI content format.

31.07.2026

AG-UI Protocol Explained: How It Compares to MDMA

Explore how AG-UI protocol streams 17 event types for real-time AI agent interaction, contrasting with MDMA's Markdown-based generative UI content format.

Read full article