Overview
ISONantic brings the power of Pydantic to ISON, providing:
Type-Safe Models
Define ISON schemas as Python classes with full type hints and IDE support.
Automatic Validation
Constraints, patterns, and custom validators ensure data integrity.
LLM Integration
Parse LLM outputs with error recovery and validation feedback.
Reference Resolution
Typed references between tables with automatic resolution.
| Feature | Pydantic | ISONantic |
|---|---|---|
| Data Format | JSON | ISON (30-70% fewer tokens) |
| Model Definition | BaseModel | TableModel, ObjectModel |
| Field Constraints | Field() | Field() (compatible API) |
| Cross-References | Manual | Reference[T] with auto-resolution |
| LLM Parsing | Not built-in | parse_llm_output() with recovery |
Installation
pip install isonantic
Or include directly in your project:
from isonantic import (
TableModel, ObjectModel, Field, Reference,
parse_ison, parse_llm_output, prompt_for_model
)
Models
ISONantic provides three model types for different ISON block types:
TableModel
For ISON tables with multiple rows:
from isonantic import TableModel, Field, Reference
class User(TableModel):
__ison_block__ = "table.users"
id: int = Field(primary_key=True)
name: str = Field(min_length=1, max_length=100)
email: str = Field(pattern=r"^[\w.-]+@[\w.-]+\.\w+$")
team: Reference["Team"]
active: bool = True
ObjectModel
For single-row configuration objects:
from isonantic import ObjectModel, Field
class Config(ObjectModel):
__ison_block__ = "object.config"
timeout: int = Field(ge=0, default=30)
debug: bool = False
version: str = "1.0"
Fields & Constraints
The Field() function supports Pydantic-compatible constraints:
from isonantic import Field
class Product(TableModel):
__ison_block__ = "table.products"
id: int = Field(primary_key=True)
name: str = Field(min_length=1, max_length=200)
price: float = Field(ge=0, le=10000)
quantity: int = Field(ge=0, default=0)
status: str = Field(choices=["active", "discontinued"])
sku: str = Field(pattern=r"^[A-Z]{3}-\d{4}$")
Available Constraints
| Parameter | Type | Description |
|---|---|---|
primary_key | bool | Mark as primary key |
unique | bool | Values must be unique |
nullable | bool | Allow null values |
default | Any | Default value |
gt, ge, lt, le | float | Numeric bounds |
min_length, max_length | int | String length |
pattern | str | Regex pattern |
choices | List | Allowed values |
description | str | Field documentation |
References
ISONantic provides typed references for cross-table relationships:
from isonantic import TableModel, Field, Reference
class Team(TableModel):
__ison_block__ = "table.teams"
id: int = Field(primary_key=True)
name: str
class User(TableModel):
__ison_block__ = "table.users"
id: int = Field(primary_key=True)
name: str
team: Reference[Team] # Typed reference!
# ISON data with references
ison = """
table.teams
id name
10 Engineering
20 "AI Research"
table.users
id name team
1 Alice :10
2 Bob :20
"""
# Parse and resolve
doc = MyDocument.parse(ison)
user = doc.users[0]
team = user.team.resolve(doc) # Returns Team object
print(team.name) # "Engineering"
Validators
@validator
Field-level validation and transformation:
from isonantic import TableModel, validator
class User(TableModel):
__ison_block__ = "table.users"
email: str
@validator("email")
def lowercase_email(cls, v):
return v.lower() # Transform value
@validator("email")
def validate_domain(cls, v):
if not v.endswith("@company.com"):
raise ValueError("Must use company email")
return v
@root_validator
Cross-field validation:
from isonantic import TableModel, root_validator
class DateRange(TableModel):
__ison_block__ = "table.ranges"
start: int
end: int
@root_validator
def validate_range(cls, values):
if values.get("start", 0) > values.get("end", 0):
raise ValueError("start must be <= end")
return values
@computed
Computed properties derived from other fields:
from isonantic import TableModel, computed
class Order(TableModel):
__ison_block__ = "table.orders"
subtotal: float
tax_rate: float
@computed
def tax(self) -> float:
return round(self.subtotal * self.tax_rate, 2)
@computed
def total(self) -> float:
return self.subtotal + self.tax
Parsing ISON
parse_ison()
Parse ISON string into validated model instances:
from isonantic import parse_ison
ison = """
table.users
id name email active
1 Alice alice@test.com true
2 Bob bob@test.com false
"""
users = parse_ison(ison, User)
for user in users:
print(f"{user.name}: {user.email}")
parse_ison_safe()
Parse with error recovery - returns valid rows even if some fail:
from isonantic import parse_ison_safe
result = parse_ison_safe(ison, User)
if result.success:
print(f"Parsed {len(result.data)} users")
else:
print(f"Errors: {result.errors}")
print(f"Valid rows: {len(result.partial_data)}")
LLM Output Parsing
ISONantic excels at parsing LLM-generated ISON, handling common formatting issues:
parse_llm_output()
from isonantic import parse_llm_output, LLMParseError
llm_response = """
Based on your query, here are the results:
table.products
id name price category
101 "Widget Pro" 29.99 electronics
102 "Gadget Plus" 49.99 electronics
Let me know if you need more details!
"""
try:
products = parse_llm_output(llm_response, Product)
for p in products:
print(f"{p.name}: ${p.price}")
except LLMParseError as e:
print(f"Parse failed: {e}")
print(f"Suggestion: {e.suggestion}")
Features
- Extracts ISON from surrounding text
- Handles markdown code blocks
- Auto-fixes common LLM mistakes (trailing commas, etc.)
- Provides suggestions for retry prompts
prompt_for_model()
Generate format instructions for LLMs:
from isonantic import prompt_for_model
prompt = prompt_for_model(Product)
# Output:
# Output format (ISON):
# table.products
# id name price category
# <int> <str> <float> <str>
#
# Constraints:
# id: primary key
# price: >= 0
Documents
ISONDocument handles multi-block ISON with reference resolution:
from isonantic import ISONDocument, TableModel
from typing import List
class Team(TableModel):
__ison_block__ = "table.teams"
id: int
name: str
class User(TableModel):
__ison_block__ = "table.users"
id: int
name: str
team: Reference[Team]
class MyApp(ISONDocument):
teams: List[Team]
users: List[User]
class Config:
block_order = ["teams", "users"]
# Parse full document
doc = MyApp.parse(ison_data)
# Access data
for user in doc.users:
team = user.team.resolve(doc)
print(f"{user.name} -> {team.name}")
# Serialize back to ISON
output = doc.to_ison()
Schema Generation
Generate JSON Schema for OpenAPI compatibility:
# JSON Schema for a single model
schema = User.model_json_schema()
print(schema)
# {
# "$schema": "https://json-schema.org/draft/2020-12/schema",
# "title": "User",
# "type": "object",
# "properties": {
# "id": {"type": "integer"},
# "name": {"type": "string", "minLength": 1, "maxLength": 100},
# "email": {"type": "string", "pattern": "..."},
# ...
# }
# }
# JSON Schema as string
json_str = User.model_json_schema_json(indent=2)
# OpenAPI-compatible schema
from isonantic import openapi_schema
api_schema = openapi_schema(MyApp)
ISONantic vs Pydantic
ISONantic and Pydantic serve different purposes and work together beautifully. Here's when to use each:
ISONantic Advantages
| Advantage | Impact |
|---|---|
| Token Efficiency | 30-70% smaller context → lower API costs |
| First-Class References | Type-safe Reference[Team] vs manual team_id: int |
| Reference Validation | Automatic validation on parse → no orphan foreign keys |
| Multi-Block Documents | Related tables in one schema (users + teams + orders) |
| LLM Output Parsing | Built-in parse_llm_output() with auto-fix for malformed output |
| Database Alignment | Tables → ISON → LLM naturally maps relational data |
Pydantic Advantages
| Advantage | Impact |
|---|---|
| Ecosystem | FastAPI, Django, massive community support |
| JSON Native | Browser/frontend compatibility out of the box |
| Maturity | Battle-tested, well-documented, stable API |
| Tooling | IDE support, linters, type checkers |
Use Both Together
The key insight: Pydantic for the edges (API), ISONantic for the core (LLM). They complement each other perfectly!
┌─────────────────────────────────────────────────┐
│ FastAPI / Django (Pydantic) │
│ JSON Request → JSON Response │
└───────────────────────┬─────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ LLM Layer (ISONantic) │
│ Token-Efficient Context → Validated Output │
└─────────────────────────────────────────────────┘
Example: Full-Stack AI App
from fastapi import FastAPI
from pydantic import BaseModel
from isonantic import TableModel, parse_llm_output, prompt_for_model
app = FastAPI()
# Pydantic for API layer (JSON - browser compatible)
class SearchRequest(BaseModel):
query: str
max_results: int = 10
class SearchResponse(BaseModel):
results: list
tokens_used: int
tokens_saved: int
# ISONantic for LLM layer (token-efficient)
class Product(TableModel):
__ison_block__ = "table.products"
id: int
name: str
price: float
category: str
@app.post("/search")
def search(request: SearchRequest) -> SearchResponse:
# Build token-efficient context (30-70% savings!)
ison_context = products_doc.to_ison()
json_equivalent = products_doc.to_json()
# Call LLM with ISON context
response = llm.complete(f"""
{ison_context}
Query: {request.query}
{prompt_for_model(Product)}
""")
# Parse and validate LLM output
results = parse_llm_output(response, Product)
# Return Pydantic response (JSON for frontend)
return SearchResponse(
results=[p.dict() for p in results],
tokens_used=len(ison_context) // 4,
tokens_saved=len(json_equivalent) // 4 - len(ison_context) // 4
)
Decision Guide
| Use Case | Best Choice |
|---|---|
| REST API endpoints | Pydantic - JSON native, FastAPI integration |
| LLM context building | ISONantic - 30-70% token savings |
| Database → LLM pipelines | ISONantic - natural table mapping |
| Multi-agent messaging | ISONantic - compact, structured data |
| Full-stack AI applications | Both together - best of both worlds |