generate_bindings: Auto-Generating Type-Safe Function Bindings from Ontology
The coomia-dip generatebindings tool automatically generates multi-language type-safe binding code from Ontology Schema, enabling user-defined functions to work with ObjectType, LinkType, and ActionType using native types without manual serialization/deserialization logic. It supports Python (Pydantic), TypeScript (Zod), Kotlin (data class), and Rust (serde) as target languages. This article details the code generator architecture, template engine, type mapping rules, incremental generation strategy, and CI/CD integration.
“Series: S5 Intelligent Decisions · Article 21 | Level: Advanced | Reading Time: 20 min
generate_bindings: Auto-Generating Type-Safe Function Bindings from Ontology
#TL;DR
The coomia-dip generate_bindings tool automatically generates multi-language type-safe binding code from Ontology Schema, enabling user-defined functions to work with ObjectType, LinkType, and ActionType using native types without manual serialization/deserialization logic. It supports Python (Pydantic), TypeScript (Zod), Kotlin (data class), and Rust (serde) as target languages. This article details the code generator architecture, template engine, type mapping rules, incremental generation strategy, and CI/CD integration.
#1. Why Auto-Generate Bindings
#1.1 Pain Points of Manual Bindings
Manual Binding vs Auto-Generation:
Manual binding:
1. Read Ontology Schema
2. Manually write data classes/interfaces
3. Write serialization/deserialization logic
4. Write type validation logic
5. Manually sync when Schema changes
-> Problems: type inconsistency, missing fields, sync lag
Auto-generation:
1. Ontology Schema updates
2. generate_bindings runs automatically
3. Type-safe native code generated
4. Compile-time/runtime type checking
-> Benefits: zero manual maintenance, type safe, always in sync
#2. Code Generator Architecture
generate_bindings Pipeline:
Ontology Schema (gRPC/JSON)
|
v
+------------------+
| Schema Parser | Parse ObjectType/LinkType/ActionType
+--------+---------+
|
v
+------------------+
| IR (Intermediate | Language-agnostic type descriptions
| Representation) |
+--------+---------+
|
+----+----+----+
v v v v
[Py] [TS] [Kt] [Rs] Language-specific generators
| | | |
v v v v
.py .ts .kt .rs Generated binding files
#3. Core Data Model
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Any
class IRType(Enum):
STRING = "string"
INTEGER = "integer"
FLOAT = "float"
BOOLEAN = "boolean"
DATETIME = "datetime"
ARRAY = "array"
MAP = "map"
OBJECT_REF = "object_ref"
ENUM = "enum"
OPTIONAL = "optional"
@dataclass
class IRField:
name: str
ir_type: IRType
description: str = ""
is_required: bool = True
default_value: Any = None
element_type: IRType | None = None
ref_type: str | None = None
enum_values: list[str] | None = None
constraints: dict[str, Any] = field(default_factory=dict)
@dataclass
class IRObjectType:
name: str
api_name: str
description: str
fields: list[IRField]
primary_key: str = "id"
@dataclass
class IRActionType:
name: str
object_type: str
parameters: list[IRField]
return_type: IRField | None = None
description: str = ""
@dataclass
class IRSchema:
namespace: str
version: str
object_types: list[IRObjectType]
link_types: list[IRLinkType]
action_types: list[IRActionType]
#4. Schema Parser
class OntologySchemaParser:
"""Ontology Schema parser"""
def parse(self, schema_data: dict) -> IRSchema:
object_types = [
self._parse_object_type(ot)
for ot in schema_data.get("objectTypes", [])
]
action_types = [
self._parse_action_type(at)
for at in schema_data.get("actionTypes", [])
]
return IRSchema(
namespace=schema_data.get("namespace", "onto"),
version=schema_data.get("version", "1.0.0"),
object_types=object_types,
link_types=[],
action_types=action_types,
)
def _parse_object_type(self, data: dict) -> IRObjectType:
fields = [
IRField(
name=f["name"],
ir_type=self._map_type(f["type"]),
description=f.get("description", ""),
is_required=f.get("required", True),
default_value=f.get("default"),
element_type=self._map_type(f["elementType"]) if "elementType" in f else None,
ref_type=f.get("refType"),
)
for f in data.get("properties", [])
]
return IRObjectType(
name=data["name"],
api_name=data.get("apiName", data["name"]),
description=data.get("description", ""),
fields=fields,
)
def _parse_action_type(self, data: dict) -> IRActionType:
params = [
IRField(
name=p["name"],
ir_type=self._map_type(p["type"]),
is_required=p.get("required", True),
)
for p in data.get("parameters", [])
]
return IRActionType(
name=data["name"],
object_type=data.get("objectType", ""),
parameters=params,
)
def _map_type(self, type_str: str) -> IRType:
mapping = {
"string": IRType.STRING, "integer": IRType.INTEGER,
"int": IRType.INTEGER, "float": IRType.FLOAT,
"double": IRType.FLOAT, "boolean": IRType.BOOLEAN,
"datetime": IRType.DATETIME, "array": IRType.ARRAY,
"map": IRType.MAP,
}
return mapping.get(type_str.lower(), IRType.STRING)
#5. Language Code Generators
#5.1 Python (Pydantic)
class PythonPydanticGenerator:
"""Python Pydantic model generator"""
TYPE_MAP = {
IRType.STRING: "str", IRType.INTEGER: "int",
IRType.FLOAT: "float", IRType.BOOLEAN: "bool",
IRType.DATETIME: "datetime", IRType.MAP: "dict[str, Any]",
}
def generate(self, schema: IRSchema) -> dict[str, str]:
lines = [
'"""Auto-generated Ontology bindings. DO NOT EDIT."""',
"from __future__ import annotations",
"from datetime import datetime",
"from typing import Any",
"from pydantic import BaseModel, Field",
"",
]
for ot in schema.object_types:
lines.extend(self._gen_object(ot))
lines.append("")
for at in schema.action_types:
lines.extend(self._gen_action(at))
lines.append("")
return {"models.py": "\n".join(lines)}
def _gen_object(self, ot: IRObjectType) -> list[str]:
lines = [f'class {ot.name}(BaseModel):']
if ot.description:
lines.append(f' """{ot.description}"""')
for f in ot.fields:
t = self._field_type(f)
if f.is_required:
lines.append(f' {f.name}: {t}')
else:
lines.append(f' {f.name}: {t} | None = None')
return lines
def _gen_action(self, at: IRActionType) -> list[str]:
lines = [f'class {at.name}Params(BaseModel):']
for p in at.parameters:
t = self._field_type(p)
if p.is_required:
lines.append(f' {p.name}: {t}')
else:
lines.append(f' {p.name}: {t} | None = None')
return lines
def _field_type(self, f: IRField) -> str:
if f.ir_type == IRType.ARRAY:
elem = self.TYPE_MAP.get(f.element_type, "Any") if f.element_type else "Any"
return f"list[{elem}]"
if f.ir_type == IRType.OBJECT_REF:
return f.ref_type or "Any"
return self.TYPE_MAP.get(f.ir_type, "Any")
#5.2 TypeScript (Zod)
class TypeScriptZodGenerator:
"""TypeScript Zod schema generator"""
TYPE_MAP = {
IRType.STRING: "z.string()", IRType.INTEGER: "z.number().int()",
IRType.FLOAT: "z.number()", IRType.BOOLEAN: "z.boolean()",
IRType.DATETIME: "z.string().datetime()",
IRType.MAP: "z.record(z.string(), z.unknown())",
}
def generate(self, schema: IRSchema) -> dict[str, str]:
lines = [
"// Auto-generated Ontology bindings. DO NOT EDIT.",
'import { z } from "zod";',
"",
]
for ot in schema.object_types:
lines.append(f"export const {ot.name}Schema = z.object({{")
for f in ot.fields:
zod = self._field_type(f)
if not f.is_required:
zod += ".optional()"
lines.append(f" {f.name}: {zod},")
lines.append("});")
lines.append(f"export type {ot.name} = z.infer<typeof {ot.name}Schema>;")
lines.append("")
return {"ontology.ts": "\n".join(lines)}
def _field_type(self, f: IRField) -> str:
if f.ir_type == IRType.ARRAY:
elem = self.TYPE_MAP.get(f.element_type, "z.unknown()") if f.element_type else "z.unknown()"
return f"z.array({elem})"
if f.ir_type == IRType.ENUM and f.enum_values:
vals = ", ".join(f'"{v}"' for v in f.enum_values)
return f"z.enum([{vals}])"
return self.TYPE_MAP.get(f.ir_type, "z.unknown()")
#6. CLI Tool
class GenerateBindingsCLI:
"""generate_bindings CLI"""
GENERATORS = {
"python": PythonPydanticGenerator,
"typescript": TypeScriptZodGenerator,
}
def run(self, args=None):
import argparse, json, os
parser = argparse.ArgumentParser(prog="generate_bindings")
parser.add_argument("--schema", required=True)
parser.add_argument("--language", required=True, choices=self.GENERATORS.keys())
parser.add_argument("--output", required=True)
parsed = parser.parse_args(args)
with open(parsed.schema) as f:
schema_data = json.load(f)
ir = OntologySchemaParser().parse(schema_data)
gen = self.GENERATORS[parsed.language]()
files = gen.generate(ir)
os.makedirs(parsed.output, exist_ok=True)
for filename, content in files.items():
path = os.path.join(parsed.output, filename)
with open(path, "w") as f:
f.write(content)
print(f"Generated: {path}")
# Usage:
# python -m onto.generate_bindings --schema schema.json --language python --output ./generated/
#7. CI/CD Integration
generate-bindings:
stage: build
script:
- python -m onto.generate_bindings
--schema control-Layer/ontology-schema.json
--language python
--output python-sdk/ontology_sdk/generated/
- python -m onto.generate_bindings
--schema control-Layer/ontology-schema.json
--language typescript
--output sdk-Layer/ts-sdk/src/generated/
- git diff --exit-code generated/
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
- control-Layer/ontology-schema.json
#8. Generated Code Example
#Input Schema
{
"namespace": "onto.demo",
"version": "1.0.0",
"objectTypes": [
{
"name": "Employee",
"description": "Employee object type",
"properties": [
{"name": "id", "type": "string", "required": true},
{"name": "name", "type": "string", "required": true},
{"name": "department", "type": "string", "required": false},
{"name": "salary", "type": "float", "required": true},
{"name": "hire_date", "type": "datetime", "required": true},
{"name": "skills", "type": "array", "elementType": "string"}
]
}
]
}
#Generated Python
"""Auto-generated Ontology bindings. DO NOT EDIT."""
from __future__ import annotations
from datetime import datetime
from typing import Any
from pydantic import BaseModel, Field
class Employee(BaseModel):
"""Employee object type"""
id: str
name: str
department: str | None = None
salary: float
hire_date: datetime
skills: list[str]
#9. Practical Example
# Using generated bindings in a custom function
from ontology_sdk.generated import Employee, PromoteEmployeeParams
def evaluate_promotion(employee: dict, performance_score: float) -> dict:
emp = Employee(**employee)
should_promote = (
performance_score >= 4.0 and
emp.salary < 500000 and
len(emp.skills) >= 5
)
if should_promote:
params = PromoteEmployeeParams(
employee_id=emp.id,
new_title="Senior Engineer",
salary_increase=emp.salary * 0.15,
)
return {
"decision": "promote",
"confidence": min(performance_score / 5.0, 1.0),
"action_params": params.model_dump(),
}
return {"decision": "maintain", "confidence": 0.8}
#10. Extensibility
class PluginRegistry:
"""Generator plugin registry"""
_generators: dict[str, type] = {}
@classmethod
def register(cls, language: str, generator_cls: type) -> None:
cls._generators[language] = generator_cls
@classmethod
def get(cls, language: str):
gen_cls = cls._generators.get(language)
if gen_cls is None:
raise ValueError(f"No generator for: {language}")
return gen_cls()
PluginRegistry.register("python", PythonPydanticGenerator)
PluginRegistry.register("typescript", TypeScriptZodGenerator)
# Users can register custom generators:
# PluginRegistry.register("swift", SwiftCodableGenerator)
#Key Takeaways
- IR intermediate representation converts Ontology Schema to language-agnostic type descriptions
- Multi-language generation supports Python/TypeScript/Kotlin/Rust target languages
- Pydantic bindings provide runtime type validation and serialization
- Zod bindings provide compile-time + runtime dual type safety for TypeScript
- CLI tool enables one-command conversion from Schema to code
- Incremental generation detects Schema diffs to avoid unnecessary regeneration
- CI/CD integration automatically triggers binding code updates on Schema changes
#Next Article
Next up: S5-22 K8s Job Executor: Deploying Custom Functions as Kubernetes Jobs details how to submit resource-intensive functions as K8s Jobs for parallel execution.
tags: #generate-bindings #code-generation #type-safety #pydantic #zod #ontology #coomia-dip