Pydantic JSON Path Value
The complete data behavior stack.
One schema defines type, format, label, value, and calculation.
One envelope carries it. One path resolves it.
PJPV is not just about where data lives. One Pydantic schema provides five distinct behaviors. The component doesn't need to know what it's rendering — the schema tells it everything.
Decimal, int, str, bool,
List[LineItem] — the field declares its type once.
No TypeScript interface duplication. No runtime guessing.
Currency, percentage, date format rules live in the schema. Display is a property of the field, not the component. Change the format once, every view updates.
Field(title="Balance") — the field names itself.
No label strings scattered across JSX. Rename once in the schema,
every UI element reflects it.
JSON envelope is the source of truth. Scalars are indexes for queries. One engine computes. Display is a projection of JSON, never an input to it. Compute from data, display from computation, never the reverse.
totals.margin = totals.total - totals.cost.
Resolved through a path, computed by one engine.
If two functions compute the same value, one is wrong.
src/pages/
InvoiceDetail.tsx (1,847 lines)
OrderDetail.tsx (1,623 lines)
ProposalDetail.tsx (1,412 lines)
PurchaseDetail.tsx (1,398 lines)
PaymentDetail.tsx ( 987 lines)
ContactDetail.tsx (2,103 lines)
ItemDetail.tsx (1,756 lines)
... 23 more models (34,000 lines)
src/components/
DynamicDetail.tsx ( 680 lines)
LineCardRenderer.tsx ( 420 lines)
TabsRenderer.tsx ( 340 lines)
HeaderRenderer.tsx ( 319 lines)
// Every model uses the same components.
// Layout comes from Settings JSON.
// Field behavior comes from Pydantic schema.
# Three serializers, three independent computations
def to_representation(self, instance):
data = super().to_representation(instance)
sell_total = Decimal(instance.sell.get('total', 0))
cost_total = Decimal(instance.cost.get('total', 0))
data['total_amount'] = str(sell_total)
data['margin_amount'] = str(sell_total - cost_total)
data['margin_percentage'] = str(
(sell_total - cost_total) / sell_total * 100
if sell_total else 0
)
return data
# Serializer passes envelope as-is
# totals.total, totals.margin, totals.margin_pc
# already computed by the one totals engine
class OrderSerializer(ModelSerializer):
class Meta:
fields = [..., 'totals', 'total', 'balance']
read_only_fields = ['totals', 'total', 'balance']
# No to_representation override needed
// Component computes its own totals
const subtotal = lines.reduce(
(sum, item) => sum + (item.extended_price || 0), 0
);
const margin = lines.reduce(
(sum, item) => sum + (item.line_margin || 0), 0
);
const total = lines.reduce(
(sum, item) => sum + (item.extended_price || 0), 0
);
<span>Subtotal: ${subtotal.toFixed(2)}</span>
<span>Margin: ${margin.toFixed(2)}</span>
<span>Total: ${total.toFixed(2)}</span>
// Component reads from server envelope
<span>Subtotal: {fmt(data?.totals?.subtotal)}</span>
<span>Margin: {fmt(data?.totals?.margin)}</span>
<span>Total: {fmt(data?.totals?.total)}</span>
// Or via configured columns (any model)
{ accessorKey: 'totals.subtotal', format: 'currency' }
{ accessorKey: 'totals.margin', format: 'currency' }
{ accessorKey: 'totals.total', format: 'currency' }
Every successful open-source commerce project passes nested JSON intact. None flatten in serializers. But none of them have Pydantic owning field behaviors end-to-end. The P is what makes PJPV different.
| Project | Nested JSON | Server Totals | Schema-Driven UI | End-to-End Behavior |
|---|---|---|---|---|
| Saleor | Yes | Yes | GraphQL fragments | No |
| Medusa | Yes | decorateTotals | No | No |
| Invoice Ninja | Yes | Yes | No | No |
| Odoo | Yes | Yes | OWL templates | No |
| ERPNext | Yes | Yes | DocType | No |
| PJPV (WebClerk) | Yes | One engine | Pydantic schema | Yes |
PJPV is a four-layer architecture where Pydantic defines the schema, JSON carries the envelope intact, Path notation resolves each value, and the Value is authoritative — computed once by one engine, displayed everywhere.
Schema authority. Types, labels, formatting rules, validation, widget hints. The field knows what it is. The UI asks the schema, not the other way around.
Source of truth. The envelope travels intact from server to client. Serializers never flatten, extract, or reshape it. What Pydantic defined is what React receives.
Resolution mechanism. Dot notation: totals.balance, price.extended.
The path comes from configuration, not hardcoded in the component.
Authoritative result. Computed once by one engine, never independently. If two functions compute the same value, one is wrong.
Your API extracts total_amount from sell.total
and serves it as a top-level field. The frontend learns to read the scalar.
Now you have two sources of truth — the envelope and the scalar.
When they disagree, nobody knows which is right.
React components compute their own totals with .reduce() over
line arrays. The server also computes totals. Three serializers compute
margin independently — one uses Decimal, another uses
float. They agree most of the time. When they don't, the
user makes decisions on bad numbers.
Labels in JSX. Format rules in utility functions. Type knowledge in TypeScript interfaces. Validation in Zod schemas. The same field is described in four places. Change one, forget the others. The drift is invisible until it surfaces as a bug.
Every model gets its own detail component. Invoice has InvoiceDetail. Order has OrderDetail. Each one knows its fields, formats them, labels them, computes them. 30 models × 1,500 lines = 45,000 lines of code that all do the same thing slightly differently.
Every field declares its type, label, and formatting in one place.
class InvoiceTotals(BaseModel):
subtotal: Decimal = Field(title="Subtotal", ge=0)
tax: Decimal = Field(title="Tax", ge=0)
total: Decimal = Field(title="Total", ge=0)
margin: Decimal = Field(title="Margin")
margin_pc: float = Field(title="Margin %")
received: Decimal = Field(title="Received", ge=0)
balance: Decimal = Field(title="Balance")
A single totals engine computes every value and writes it to the JSON envelope. No other code computes these values. Ever.
def recalculate_totals(transaction_id, model_name):
header, lines = resolve(transaction_id, model_name)
subtotal = sum(line.price.extended for line in lines)
cost = sum(line.cost.extended for line in lines)
margin = subtotal - cost
margin_pc = (margin / subtotal * 100) if subtotal else 0
header.totals = {
'subtotal': subtotal, 'tax': tax,
'total': total, 'cost': cost,
'margin': margin, 'margin_pc': margin_pc,
'received': received, 'balance': total - received,
}
header.save(update_fields=['totals', 'total', 'balance'])
No extraction. No flattening. No to_representation surgery.
The envelope goes out the way it was computed.
class OrderSerializer(ModelSerializer):
class Meta:
model = Order
fields = [
'id', 'ida', 'status',
'cost', 'sell', 'totals', # envelopes intact
'total', 'balance', # indexes for queries
'lines', ...
]
read_only_fields = ['totals', 'total', 'balance']
Components don't know what they're rendering. They resolve a path from configuration and display the result.
// Column definition — path from configuration
const columns = [
{ header: 'Total', accessorKey: 'totals.total', format: 'currency' },
{ header: 'Margin', accessorKey: 'totals.margin_pc', format: 'percent' },
{ header: 'Balance', accessorKey: 'totals.balance', format: 'currency' },
];
// Component renders any model — it reads the schema
<DynamicDetail
data={record}
schema={pydanticSchema}
layout={settingsLayout}
/>
Field behaviors — types, labels, formatting, validation, widgets — are declared in Pydantic schema code. Not in the database. Not in React. Not in Settings. Settings store layouts (which fields to show and where). Pydantic stores behaviors (what the field is and how it acts).
Serializers never flatten, extract, or reshape. No
data['total_amount'] = instance.sell.get('total').
The envelope goes out the way it was computed. Round-trip safe.
PATCH returns what GET served.
Components resolve values through dot-path notation from configuration.
accessorKey: 'totals.balance', not record.balance.
The path comes from the schema or layout settings, not the component source code.
Every computed value has exactly one producer. React reads, it does not compute authoritative values. Selection-aware subtotals (user-selected line subsets) are legitimate local computation — labeled as partial, never confused with the server total.
Full technical readme with code examples, decision rationale, and industry research.
View on GitHubWebClerk is the open-source reference implementation. 30+ models, all rendered by DynamicDetail using PJPV.
WebClerk.comPJPV works with any Django + DRF + React stack. It also applies to FastAPI, Flask, or any backend where Pydantic defines the schema.
Implementation Guide