# Protocol Buffers — API: The Transform API for proto3: Four Routes, Both Directions

Most schema converters are one-way. You can get *out* of the format or *into* it, rarely both, and the documentation usually leaves you to discover which by trying. So let us answer the direction question for Protocol Buffers first and in one sentence: **proto3 decodes and encodes.** The key `protobuf` (alias `proto`) appears in both the decode list and the encode list, so it is valid as a source and as a target on every route below.

# The Transform API for proto3: Four Routes, Both Directions

Most schema converters are one-way. You can get *out* of the format or *into* it, rarely both, and
the documentation usually leaves you to discover which by trying. So let us answer the direction
question for Protocol Buffers first and in one sentence: **proto3 decodes and encodes.** The key
`protobuf` (alias `proto`) appears in both the decode list and the encode list, so it is valid as a
source and as a target on every route below.

For contrast, and because honesty about direction is the point: `odm` decodes only - it is authored
documentation, not a generated artifact - and `synapse` encodes only, returning an error that tells
you what to do instead: `'synapse' is encode-only: a Synapse schema is plain draft-07 JSON Schema -
decode it with the 'jsonschema' format.` Protobuf has no such asterisk.

Four routes carry the proto3 work this article covers. All are `POST`, all live under
`{host}/graph/transform/`, all take `Authorization: Bearer $TOKEN` and
`Content-Type: application/json`, and all are project-scoped:

| Route | Role | What it does with proto3 |
|---|---|---|
| `schema/map/{projectId}` | Viewer | stateless conversion in or out; returns schema + plan |
| `schema/import/{projectId}` | Admin | writes a `.proto` into a project as governed model |
| `schema/export/{projectId}` | Viewer | emits a project's schema as a `.proto` |
| `plan/execute/{projectId}` | Viewer | replays a stored plan against the same source |

A fifth route, `schema/mapImport/{projectId}`, imports through the mapping engine with a `dryRun`
flag (Admin to write, Viewer for the dry run). And one role exception across the mapping routes:
`"mapping": { "kind": "ai" }` requires Editor or Admin membership rather than Viewer.

Every response uses the same envelope: `success`, `lossiness`, `errors`, and one payload key
(`schema`, `projectId`, or `summary`), plus `plan` on the mapping routes. `success: true` means the
call ran, not that nothing changed - the `lossiness` array is where the change report lives.

## schema/map - conversion without touching anything

The workhorse. It scopes authorization to the project and reads nothing from it; every call is
inherently a dry run.

Here is a catalog schema on its way to JSON Schema. The `.proto` text travels as a JSON string:

```bash
curl -sS -X POST \
  "https://coremodels.example.com/graph/transform/schema/map/$PROJECT_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "sourceFormat": "protobuf",
    "sourceSchema": "syntax = \"proto3\";\n\npackage acme.catalog;\n\nmessage Product {\n  string sku = 1;\n  string title = 2;\n  optional string description = 3;\n  double list_price = 4;\n  repeated string tags = 5;\n  Availability availability = 6;\n}\n\nenum Availability {\n  AVAILABILITY_UNSPECIFIED = 0;\n  IN_STOCK = 1;\n  BACKORDERED = 2;\n  DISCONTINUED = 3;\n}",
    "targetFormat": "jsonschema",
    "targetHintFormat": "protobuf",
    "targetHintSchema": "syntax = \"proto3\";\n\npackage acme.catalog;\n\nmessage Product {\n  string sku = 1;\n  string title = 2;\n  optional string description = 3;\n  double list_price = 4;\n  repeated string tags = 5;\n  Availability availability = 6;\n}\n\nenum Availability {\n  AVAILABILITY_UNSPECIFIED = 0;\n  IN_STOCK = 1;\n  BACKORDERED = 2;\n  DISCONTINUED = 3;\n}",
    "mapping": { "kind": "inferred" }
  }'
```

The response:

```jsonc
{
  "success": true,
  "lossiness": [],
  "errors": [],
  "schema": {
    "type": "object",
    "properties": {
      "sku": { "type": "string" },
      "title": { "type": "string" },
      "description": { "type": "string" },
      "list_price": { "type": "number" },
      "tags": { "type": "array", "items": { "type": "string" } },
      "availability": {
        "enum": ["AVAILABILITYUNSPECIFIED", "INSTOCK", "BACKORDERED", "DISCONTINUED"]
      }
    },
    "required": ["sku", "title", "list_price", "availability"]
  },
  "plan": { "operations": [ /* one operation per mapped construct */ ] }
}
```

An empty `lossiness` array is a real result, not a placeholder: this file uses only constructs that
have exact homes on the other side. Note `description` sitting outside `required` - it carried the
explicit `optional` label - and `tags` outside it as well, because it is a collection.

`targetHintSchema` is repeated verbatim here because a straight format conversion maps a schema to
itself. That is the identity case; the interesting case is next.

## schema/map with a real target hint

When the hint is a *different* schema, `inferred` aligns the two by label and compatible type, and
the plan records which target construct each source construct landed on. Send a telemetry `.proto`
toward a JSON Schema that describes the same concept, and ask for Postgres DDL out:

```json
{
  "sourceFormat": "protobuf",
  "sourceSchema": "syntax = \"proto3\";\n\npackage acme.telemetry;\n\nimport \"google/protobuf/timestamp.proto\";\n\nmessage DeviceReading {\n  string device_id = 1;\n  sint32 temperature_c = 2;\n  uint32 battery_pct = 3;\n  google.protobuf.Timestamp read_at = 4;\n  optional string firmware = 5;\n}",
  "targetFormat": "sql",
  "vendor": "postgres",
  "targetHintFormat": "jsonschema",
  "targetHintSchema": "{ \"$id\": \"DeviceReading\", \"type\": \"object\", \"title\": \"DeviceReading\", \"properties\": { \"device_id\": { \"type\": \"string\" }, \"temperature_c\": { \"type\": \"integer\" }, \"battery_pct\": { \"type\": \"integer\" }, \"read_at\": { \"type\": \"string\", \"format\": \"date-time\" }, \"firmware\": { \"type\": \"string\" } }, \"required\": [\"device_id\", \"temperature_c\", \"battery_pct\"] }",
  "mapping": { "kind": "inferred" }
}
```

The `schema` field comes back as a string for text targets:

```sql
CREATE TABLE "DeviceReading" (
  "device_id" VARCHAR(255) NOT NULL,
  "temperature_c" INTEGER NOT NULL,
  "battery_pct" INTEGER NOT NULL,
  "read_at" TIMESTAMP,
  "firmware" VARCHAR(255)
);
```

and the plan shows the alignment explicitly - each operation naming the hint's element id:

```json
{
  "kind": "ElementMapping",
  "origin": "Inferred",
  "sourceElementIds": ["DeviceReadingTemperatureC"],
  "targetElementIds": ["DeviceReading::temperature_c"]
}
```

`"origin": "Inferred"` is the label saying *this was a heuristic match*. The ledger for this call
carries three records: the unresolved import, and one `TypeApproximation` each for `sint32` and
`uint32` - "encoding semantics are not modelled; approximated as Integer with the verbatim type
preserved." Zigzag and unsigned encodings are exactly the facts a table cannot hold, and exactly the
facts most hand conversions never mention.

`vendor` applies to `sql` output only (`postgres` is the default, with `mysql` and `sqlserver` also
accepted). Protobuf output takes no format-specific option: the knobs that shape a `.proto` are the
mapping kind and the hint.

## schema/import - a .proto becomes a governed model

This route writes, so it needs Admin:

```bash
curl -sS -X POST \
  "https://coremodels.example.com/graph/transform/schema/import/$PROJECT_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "format": "protobuf",
    "schema": "syntax = \"proto3\";\n\npackage acme.catalog;\n\nmessage Product {\n  string sku = 1;\n  string title = 2;\n  optional string description = 3;\n  double list_price = 4;\n  repeated string tags = 5;\n  Availability availability = 6;\n}\n\nenum Availability {\n  AVAILABILITY_UNSPECIFIED = 0;\n  IN_STOCK = 1;\n  BACKORDERED = 2;\n  DISCONTINUED = 3;\n}",
    "spaces": []
  }'
```

```json
{ "success": true, "lossiness": [], "errors": [], "projectId": "..." }
```

Each message becomes a Type, each field an Element under its wire name, each enum a Taxonomy with
its values as terms in order. Nested messages become their own Types that remember which message
enclosed them. `spaces` is optional - an empty array means the project's main space.

One caveat we would rather state than have you discover. The wire-level facts a `.proto` carries -
field numbers, the package, file and message options, `reserved` statements, the verbatim scalar
tokens - ride in the transform's annotation channel, which is *not* persisted into a project's
schema store. Element optionality and cross-standard mappings are persisted; field numbers are not.
So an import followed later by an export gives you a schema-equivalent `.proto`, not the original
wire contract. When those facts matter, keep the conversion on the stateless routes, where they
survive intact. When what you want governed is the *meaning*, import is exactly right.

## schema/export - a project becomes a .proto

```bash
curl -sS -X POST \
  "https://coremodels.example.com/graph/transform/schema/export/$PROJECT_ID" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "format": "protobuf" }'
```

For a project holding a `Customer` type with a required `customer_key` (Integer) and `full_name`
(String), an optional `signup_date` (DateTime), and a `status` element pointing at a three-term
`CustomerStatus` taxonomy, the `schema` string is:

```proto
syntax = "proto3";

message Customer {
  int64 customer_key = 1;
  string full_name = 2;
  optional string signup_date = 3;
  optional CustomerStatus status = 4;
}

enum CustomerStatus {
  draft = 0;
  active = 1;
  closed = 2;
}
```

Everything visible here is a rule, not a coincidence. Integer becomes `int64`, Double `double`,
Boolean `bool`, and everything else `string`. Elements that were never required gain the explicit
`optional` label so their presence is tracked. Field numbers are minted sequentially in declaration
order - and the minting skips numbers already in use and the implementation-reserved 19000–19999
range, so the output is wire-safe by construction. Enum values are numbered from zero in term order.

The ledger for this export has one entry: `TypeApproximation` on `Element[CustomerSignupDate]` -
"proto3 has no date-time scalar; DateTime degrades to string." That is true of proto3 generally,
which is why the ecosystem uses `google.protobuf.Timestamp`; a definition that arrived from a
`.proto` carries that token verbatim and re-emits it, while one that never saw proto3 has no token
to re-emit. The ledger tells you which case you are in.

`{ "format": "proto" }` behaves identically - the alias is resolved at dispatch.

## plan/execute - replay, gated the same way

The `plan` from any mapping call is a reviewable, storable artifact. Replaying it takes the same
source, the plan as a **string**, and a target format:

```json
{
  "sourceFormat": "protobuf",
  "sourceSchema": "<the same .proto text>",
  "plan": "{\"operations\":[{\"kind\":\"TypeMapping\",\"origin\":\"Inferred\",\"sourceTypeId\":\"ShipmentEvent\",\"targetTypeId\":\"ShipmentEvent\",\"targetLabel\":\"ShipmentEvent\"}]}",
  "targetFormat": "protobuf"
}
```

A stored plan earns no shortcut: it goes through the identical validation gate as a freshly produced
one, then executes deterministically. Same plan plus same source produces the same output, every
run.

## When it says no

The decode side is strict on exactly one axis - the proto3 language - and forgiving everywhere else.
Errors arrive in the envelope, never as a stack trace:

```json
{
  "success": false,
  "lossiness": [],
  "errors": [
    { "path": "$", "message": "Only proto3 is supported; the file declares syntax \"proto2\"." }
  ],
  "schema": null
}
```

Other refusals you may meet: `'required' fields are proto2; only proto3 is supported.`, `The protobuf
schema is empty.`, and parse errors that point at the token - `Expected ';' but found '}'.` A
misspelled format key answers with the whole list: `Unknown schema format 'protobuff'. Use:
jsonschema | shex | avro | jsonld | sql | osi | osi-json | owl | linkml | protobuf | odcs | odm.`

What is *not* a failure: a `service` block, an `extend` block, an unresolvable import, a `map` field,
or a missing `syntax` statement. Each decodes as far as it can and files a lossiness record instead.
A decode never hard-fails on a construct it can skip.

The transform section of the CoreModels documentation lists every route with its role and a
ready-to-paste body for each format.
