Back to Blog

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.

CoomiaPublished on September 13, 20257 min read
Share this articleTwitter / X

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

Code
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

Code
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

Python
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

Python
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)

Python
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)

Python
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

Python
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

YAML
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

JSON
{
  "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

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

Python
# 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

Python
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

  1. IR intermediate representation converts Ontology Schema to language-agnostic type descriptions
  2. Multi-language generation supports Python/TypeScript/Kotlin/Rust target languages
  3. Pydantic bindings provide runtime type validation and serialization
  4. Zod bindings provide compile-time + runtime dual type safety for TypeScript
  5. CLI tool enables one-command conversion from Schema to code
  6. Incremental generation detects Schema diffs to avoid unnecessary regeneration
  7. 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