Guidance for human contributors and AI coding agents working in this repository. This project is
@asyncapi/protobuf-schema-parser: an @asyncapi/parser
schema-parser plugin that converts Protocol Buffers (.proto) message definitions into AsyncAPI
(JSON Schema based) schemas.
Keep changes focused, factual and consistent with the conventions below. When behaviour changes, update or add a fixture (see section 6) so the mapping stays covered by tests.
- Language: TypeScript (compiled with
tsc,strictmode on). - Runtime: Node.js
>= 18. The library is also browser-compatible (sideEffects: false), so avoid Node-only APIs insrc/outside of test code. - Core dependencies:
@asyncapi/parser— the host parser this plugin registers with; source of theSchemaParsertype and the schema types under@asyncapi/parser/esm/....protobufjs— parses the raw.protostring into a reflection tree.@types/protocol-buffers-schema— type definitions.
- Build output: dual module builds — ES modules in
esm/(build:esm) and CommonJS incjs/(build:cjs).package.jsonpointsmainatcjs/,moduleatesm/, andtypesatesm/index.d.ts. - Tooling: Jest (
@swc/jest) for tests, ESLint for linting,semantic-releasefor releases,markdown-tocfor the README table of contents.
src/
index.ts Parser factory + @asyncapi/parser schema-parser contract
protoj2jsonSchema.ts Core converter (Proto2JsonSchema class)
primitive-types.ts Scalar Protobuf -> JSON Schema type/format map
google-types.ts Bundled google/type/*.proto definitions
pathUtils.ts Import path resolution helpers
protoc-gen-validate.ts (validate.rules) option mapping
protovalidate.ts (buf.validate.field) option mapping + shared validator logic
test/
parser.spec.ts Test suite
documents/*.yaml AsyncAPI input fixtures
documents/*.result.json Expected parsed output for each fixture
esm/ cjs/ Generated build output (do not edit by hand; git-ignored)
Edit only src/ and test/. Never hand-edit esm/ or cjs/ — they are produced by the build and
are git-ignored.
- Follow
.editorconfigand.eslintrc: 2-space indentation, single quotes, semicolons required, LF line endings, final newline,no-var,prefer-const,object-shorthand,prefer-template. - Run
npm run lint(ornpm run lint:fix) before proposing changes. Lint must pass with--max-warnings 0. - Prefer small, private helper methods on the converter class over deeply nested logic. The
sonarjs/cognitive-complexityrule is a warning; where a function is unavoidably complex, the codebase disables it with a scopedeslint-disablecomment rather than leaving warnings. - Preserve the existing use of non-standard
x-keywords (see section 5) — they are part of the output contract that downstream tooling relies on. - Keep the library free of Node-only or filesystem APIs (browser compatibility). File reads belong in tests only.
- Commit messages follow Conventional Commits — releases are
driven by the commit prefix (
fix:,feat:,docs:,chore:,test:,refactor:; add!for breaking changes).
src/index.ts exports ProtoBuffSchemaParser() (both a named and the default export). It returns an
object implementing the SchemaParser interface from @asyncapi/parser:
getMimeTypes()— returns the schema formats this parser handles:application/vnd.google.protobuf;version=2andapplication/vnd.google.protobuf;version=3.@asyncapi/parserroutes any message payload whoseschemaFormatmatches one of these strings to this parser.parse(input)— receivesinput.data(the raw Protobuf string) and returns the converted AsyncAPI schema. Delegates toproto2jsonSchema(input.data).validate(input)— returnsPromise<SchemaValidateResult[]>. An empty array means valid; any parse error is caught and returned as a single result withmessageandinput.path(the Protobuf parser does not provide an error path).
A consumer registers the parser with parser.registerSchemaParser(ProtoBuffSchemaParser()). Keep
this three-method surface stable; changing method names or the MIME-type strings is a breaking
change.
The converter (src/protoj2jsonSchema.ts, class Proto2JsonSchema) parses the schema with
protobufjs (options keepCase: true, alternateCommentMode: true), resolves the root message, and
walks the tree. Core rules:
- Message ->
{ title, type: 'object', properties, required }. Therequiredarray is dropped when empty. The message comment becomesdescription. - Root selection -> the message not used as a field type by any other message is the root. If
more than one candidate remains, one must be annotated
@RootNode, else throwFound more than one root proto messages. Zero candidates throwsNot found a root proto messages. - Scalar field -> looked up in
PrimitiveTypes.PRIMITIVE_TYPES_WITH_LIMITS(orPRIMITIVE_TYPES_MINIMALwhenprimitiveTypesWithLimitsisfalse); setstype,format,x-primitive, and numericminimum/maximumwhere defined. - Message-typed field -> the referenced message compiled inline, plus
x-type= Protobuf type name. - Enum ->
{ title, type: 'string', enum: [names], 'x-enum-mapping': { name: number } }; a field referencing an enum also getsx-type. repeatedfield ->{ type: 'array', items: <field schema> }. Supports@MinItems/@MaxItemsand validator-frameworkmin_items/max_items/unique/items.oneofwith 2+ members -> a property named after theoneofholding{ oneOf: [...] }; each variant carriesx-oneof-item= originating field name. Single-member syntheticoneofs (proto3optional, names starting with_) are filtered out.- Required membership is additive: proto2
required, non-optional proto3 field, or@Required. - Recursion guard: if a message type appears twice on the current stack branch, stop descending.
- Comments become
descriptionafter@-annotations are stripped. Field annotations (@Min/@Max/@Pattern/@ExclusiveMinimum/@ExclusiveMaximum/@MultipleOf/@MinLength/@MaxLength/@MinItems/@MaxItems/@Default/@Example/@Required) map to the corresponding JSON Schema keywords. - Head
@Optionannotations configure the converter (currently onlyprimitiveTypesWithLimits). - Validation frameworks:
(validate.rules)(protoc-gen-validate,src/protoc-gen-validate.ts) and(buf.validate.field)(protovalidate,src/protovalidate.ts) are translated to JSON Schema validators (const,lt/lte/gt/gte-> exclusive/inclusiveminimum/maximum,in/not_in->oneOf/not, string length/pattern/format rules,ignore_empty-> optional).
Imports/refs are unsupported except the bundled well-known definitions (google/protobuf/* via
protobufjs, google/type/* via google-types.ts, and the two validation .proto files). Any
other import throws.
The x- keywords (x-primitive, x-type, x-enum-mapping, x-oneof-item) are an intentional part
of the output. Do not remove or rename them without treating it as a breaking change.
- Tests live in
test/parser.spec.tsand run vianpm test(jest --coverage). - Each case parses an AsyncAPI YAML fixture from
test/documents/*.yamlthrough a real@asyncapi/parserParserwithProtoBuffSchemaParserregistered, then deep-equals the result against the matching*.result.json(withx-parser-*internals stripped). - When you change the mapping, add or update a fixture pair (
<name>.yaml+<name>.result.json). To regenerate expected outputs, temporarily setUPDATE_RESULTS = trueinparser.spec.ts, run the suite, review the diff, then set it back tofalse. Never commit withUPDATE_RESULTS = true. - Invalid-input fixtures (e.g.
invalid.multiple_root.yaml,invalid.schema-empty.yaml) assert that parsing fails and produces diagnostics. Add negative cases for new error conditions. - Cover both proto2 and proto3 where relevant, and keep the
googlewell-known-type and validation fixtures green when touching import handling or the validator mappers.
- A converted payload always has exactly one root object (JSON Schema has a single root). Multiple
unparented messages require
@RootNodedisambiguation. - The converter runs synchronously;
parsereturns the schema value directly (theSchemaParserinterface also permits a Promise). protobufjsis parsed withkeepCase: true— field/enum names are preserved verbatim; do not camel-case them.- The scalar map is the single source of truth for scalar output. Derive the "minimal" (no
minimum/maximum) variant from the "with limits" map rather than duplicating entries. - Error messages are user-facing (surfaced through
validatediagnostics) — keep them clear and specific. - Keep
README.mdin sync with mapping/annotation/option changes, and regenerate its table of contents withnpm run generate:readme:tocwhen headings change.
Input (Protobuf embedded in an AsyncAPI message payload):
message Point {
int32 x = 1;
int32 y = 2;
optional string label = 3;
}Output (converted AsyncAPI schema, abbreviated):
{
"title": "Point",
"type": "object",
"required": ["x", "y"],
"properties": {
"x": { "type": "integer", "format": "int32", "minimum": -2147483648, "maximum": 2147483647, "x-primitive": "int32" },
"y": { "type": "integer", "format": "int32", "minimum": -2147483648, "maximum": 2147483647, "x-primitive": "int32" },
"label": { "type": "string", "x-primitive": "string" }
}
}Public AsyncAPI and Protobuf ecosystem projects only:
@asyncapi/parser— the host parser this plugin extends.- Protocol Buffers — the input schema language.
protobufjs— the underlying.protoparser.- protoc-gen-validate and protovalidate — supported validation frameworks.