Parser contract
This reference defines the current file-reading and parser-facing contract in
asyncapi-generator-core.
Public file-loading entry point
val result = AsyncApiDocumentLoader().load(file)
val document = result.document
val warnings = result.warnings
val renderedWarnings = result.formatWarnings()
val sourceFiles = result.sourceFiles
Each call creates an isolated AsyncApiContext. The loader accepts .yaml,
.yml, and .json files, reads and structurally parses the root document,
eagerly loads supported external references, and runs semantic validation.
Reader, parser, external-reference, and validation errors are thrown. A
successful result contains the existing AsyncApiDocument domain model and an
immutable list of validation warnings. Warnings found while validating external
documents or selected fragments are returned with root warnings, deduplicated
by rule and source location, and are never logged by parser infrastructure.
formatWarnings() returns an empty string when no warnings exist and otherwise
renders source-aware snippets.
sourceFiles is an immutable, deterministic set of canonical File values. It
contains the root document, every loaded external document, and every external
native Avro or Protobuf schema asset. Canonical files appear once even when
multiple references or cycles reach them. Build frontends use this set for
incremental input tracking.
The loader is stateless. Every call owns a new context, repositories, warning collector, resource budget, and reader instances. Repeated and concurrent calls therefore cannot share parser state.
Bundling and generation are not performed by this API.
The loader, load result, domain model, validation findings, source locations, and caller-relevant diagnostics and exceptions form the supported API. The document tree, concrete readers, cursor types, contexts, repositories, registries, resolvers, version implementations, individual parsers, and individual validators are internal to the core module.
Reader contract
Inside the loader, DocumentReaderRegistry selects a reader by case-insensitive
file extension. The registry, its DocumentSource overload, and both reader
implementations are internal; external callers use AsyncApiDocumentLoader.
Both readers return an InputDocument whose root is a DocumentNode and whose
contents use the same semantic node categories:
These shared types live in the neutral document package. Reader
implementations produce them, while the parser and other source-aware consumers
depend on them without depending on a concrete YAML or JSON reader.
| Node | Plain runtime value | Location |
|---|---|---|
DocumentObject |
Map<String, Any?> |
Object start; every member also has a key location |
DocumentArray |
List<Any?> |
Array start; every element has its own location |
DocumentString |
String |
Scalar token |
DocumentNumber |
Int, Long, BigInteger, Double, or BigDecimal |
Scalar token |
DocumentBoolean |
Boolean |
Scalar token |
DocumentNull |
null |
Null token |
Readers reject empty documents, malformed syntax, invalid mapping keys, and
duplicate keys with DocumentReadException. A syntactically valid document may
have an object, array, scalar, or null root; readers do not apply the AsyncAPI
requirement that the root be an object.
Document objects always have string member names. JSON enforces this in its
grammar; YAML mapping keys must resolve as strings and cannot be collections,
numbers, booleans, or null. Quoted YAML keys such as "true" and "42" remain
valid strings. This prevents YAML-only key coercion from producing a document
tree that could not be represented by equivalent JSON. YAML merge keys are
rejected rather than being exposed as literal << members; shared members must
be written explicitly.
YAML custom tags and unsupported node kinds are malformed input.
Only JSON-compatible numeric spellings are accepted: YAML-only octal forms,
non-finite numbers, and other YAML numeric representations are rejected rather
than silently becoming different values. Timestamp-looking values remain
strings. Plain yes, no, on, and off remain strings, while actual
true and false tokens become booleans. A UTF-8 BOM is accepted, CRLF input
retains correct source locations, and additional YAML or JSON documents after
the root are rejected.
The AsyncAPI parser requires the root cursor to contain an object. Any other
root produces an unexpected-value-type parser diagnostic at the root source
location. This keeps format syntax errors separate from AsyncAPI structural
errors.
AsyncAPI specification versions
Before parsing the domain structure, AsyncApiParser requires asyncapi to be
a string in major.minor.patch form with an optional alphanumeric suffix. The
declared value is preserved in AsyncApiDocument, while its major/minor line
selects an AsyncApiParserProfile. Patch releases share the same profile.
The parser recognizes the published 3.0 and 3.1 specification lines but only
the 3.0 parser profile is currently implemented. Therefore 3.0.x documents
are supported, including suffixed values such as 3.0.0-rc1; 3.1.x produces
a diagnostic explaining that the version is known but its parser profile has
not been implemented. Other version lines are unsupported rather than being
silently interpreted as 3.0.
Profiles are carried by parser nodes. Complete external AsyncAPI documents
select their own profile, while raw external fragments inherit the profile of
the reference that loads them. Version-dependent parsing must use this profile
instead of comparing the raw asyncapi string.
File access failures are normalized as UnreadableDocument; Jackson and
SnakeYAML exceptions do not escape as the top-level failure. Both readers apply
the same default limits of 20 MiB for UTF-8 input and decoded document length,
plus a nesting depth of 100. YAML additionally permits at most 50 expanded
collection aliases. Numeric tokens are limited to 1,000 characters in both
formats. Size, depth, alias, and numeric-token violations are reported as
ResourceLimitExceeded. An unrecognized SnakeYAML exception is
MalformedDocument, not a resource-limit failure.
DocumentReadException exposes structured information independently of its
message: every subtype has file, failures with a known source mark have
location, DuplicateKey has memberName, UnsupportedFormat has format,
and ResourceLimitExceeded has limit and maximum. The original cause is
retained where a library, decoding, or file-access failure exists.
File input is decoded as strict UTF-8. Malformed byte sequences are rejected as
MalformedDocument rather than being silently replaced before syntax parsing.
YAML presentation details such as quoting and block style do not survive as
semantic data. Quoted numbers and booleans remain strings. JSON-compatible YAML
booleans true and false become booleans; YAML 1.1 words such as yes, no,
on, and off remain strings.
YAML and JSON use the same numeric representation. Integers use Int or Long
when in range and BigInteger otherwise. Decimals remain Double when their
lexical value survives the conversion exactly; values that would lose decimal
precision or overflow use BigDecimal. This preserves ordinary runtime values
while preventing format-dependent truncation.
Parser cursor API
ParserNode is a source-aware cursor over one DocumentNode. Structural
navigation is available only after selecting an object or array view.
| Owner | Operation | Contract |
|---|---|---|
ParserNode |
expectObject() |
Requires an object and returns its object navigation view |
ParserNode |
expectArray() |
Requires an array and returns its indexed navigation view |
ParserNode |
expect<T>() |
Checks the complete requested Kotlin runtime type, including nested list and map values |
ParserNode |
toPlainValue() |
Recursively removes source metadata and returns maps, lists, scalars, or null |
ParserObjectNode |
required(name) |
Requires the named member to be present and returns its cursor |
ParserObjectNode |
optional(name) |
Returns the member cursor or null when absent |
ParserObjectNode |
members() |
Returns one cursor per member, preserving names and paths |
ParserObjectNode |
membersStartingWith(prefix) |
Returns matching member cursors with their original names and paths |
ParserObjectNode |
expectOnlyMembers(...) |
Rejects members outside an object's supported set and allowed specification extensions |
ParserArrayNode |
elements() |
Returns one cursor per element with indexed paths |
expect<T>() performs type checking, not coercion. A string is not converted to
a boolean or number, and a scalar is not wrapped in a collection. Map keys and
nested collection elements are checked recursively, so
expect<Map<String, List<Boolean>>>() cannot hide an invalid nested value behind
an unchecked cast.
expect<Any?>() and toPlainValue() both allow any JSON-compatible value.
expect<T>() should be preferred when the AsyncAPI field has a defined shape;
toPlainValue() communicates that source metadata is intentionally discarded
at a free-form boundary.
Fixed-field object policy
The AsyncAPI 3.0 parser profile owns the allowed-member set for each ordinary
fixed-field object. Domain parsers select the relevant object type and do not
repeat large field sets locally. An unknown non-extension member produces
parser.unexpected-object-member at the member key. A specification extension
is accepted when its name matches x- followed by letters, digits, dots,
underscores, or hyphens.
This policy applies to ordinary AsyncAPI objects such as Info, Server, Channel, Message, Operation, Components, Security Scheme, and their traits and nested fixed-field objects. It does not reinterpret patterned maps, bindings, Schema Objects, or deliberately free-form values as fixed-field objects.
Unconditional required fields are parser-owned. In particular, an inline
Operation Object requires both action and channel. Whether the referenced
channel resolves to the correct semantic target remains validator-owned.
Absent and null values
Absence and explicit null are different parser states.
| Input state | Object view optional("field") |
Object view required("field") |
expect<String>() |
expect<String?>() |
|---|---|---|---|---|
| Member absent | null |
Missing-required-member diagnostic | Not applicable | Not applicable |
Member is null |
Cursor over DocumentNull |
Cursor over DocumentNull |
Unexpected-value-type diagnostic | null |
| Member is a string | Cursor over DocumentString |
Cursor over DocumentString |
String value | String value |
Callers must not use a nullable expectation merely to make malformed nulls
disappear. Use it only when the corresponding domain contract permits explicit
null. When presence itself is significant, retain the member cursor separately;
SchemaParser does this for default through its defaultSet flag.
Channel.address is a specification-defined nullable exception: absence and
explicit null both produce a null domain value, while a non-string, non-null
value is a structural parser error. JSON Schema nullability is expressed with a
type union such as type: [string, "null"]; a Schema Object type: null is not
a nullable schema.
Parser diagnostics
Strict parser failures throw
AsyncApiParseException.ParserDiagnosticFailure. Its diagnostic is structured
data; the exception message is a human-readable rendering with a source snippet.
Every ParserDiagnostic exposes:
- a stable category and string code;
- expected type or condition;
- actual value category and actual plain value when applicable;
- parser path; and
SourceLocation, containing source identifier, file, one-based line, and one-based column.
Current categories are:
| Code | Meaning |
|---|---|
parser.missing-required-member |
An object does not contain a required member |
parser.unexpected-object-member |
An object contains a member that its parser does not recognize |
parser.unexpected-value-type |
A value or nested value has the wrong runtime type |
parser.invalid-specification-version |
The asyncapi value does not have the required version form |
parser.unsupported-specification-version |
The declared version has no implemented parser profile |
parser.invalid-reference |
A reference is not a supported URI reference with JSON Pointer semantics |
parser.reference-document-not-found |
The external document does not exist or is unreadable |
parser.reference-target-not-found |
The JSON Pointer target does not exist in the loaded document |
parser.load-resource-limit-exceeded |
One complete load exceeded a configured source/reference resource budget |
Diagnostic messages may include targeted hints for common quoted scalar mistakes. The category and structured fields, rather than rendered prose, are the stable surface for programmatic assertions.
Internal identity and display paths
Repositories identify nodes with a canonical source identifier and typed
member/index segments. A rendered dot path is never parsed to recover identity.
This distinguishes a component named A.properties.x from property x below
component A, and distinguishes an object member named "0" from array index
0.
Simple names keep the existing display form. Ambiguous names use quoted bracket notation with JSON string escaping. For example:
contract.root.components.schemas.Order
contract.root.components.schemas["Order.properties.id"]
contract.root.values["0"]
contract.root.values[0]
SourceLocation.path and parser diagnostic paths are presentation values. The
source and model repositories use the segment-aware address directly.
Reference behavior
A Reference Object must be an object containing a string $ref. A missing
member produces a missing-required-member diagnostic where a Reference Object
is required; an explicit null or a non-string value produces an
unexpected-value-type diagnostic at $ref. For reference-versus-inline fields,
a present $ref controls discrimination. Once it is a valid string, sibling
members are ignored as required by AsyncAPI 3.0 and are not checked against the
inline object's member policy. Domain parsers attach the concrete
ReferenceCategoryKey used to parse external fragments; the category is not
inferred from substrings in the reference path.
Internal references do not load a file. Local relative paths and file: URIs
are resolved relative to the source document that owns the reference. HTTP and
other remote schemes are unsupported. Canonical file paths distinguish
same-named files in different directories. URI percent encoding is decoded for
file paths, and JSON Pointer ~0 and ~1 escapes are supported when selecting
targets. Numeric-looking object keys remain object members rather than being
mistaken for array indexes.
External targets have two modes:
- A target document with an
asyncapimember is parsed and validated as a full AsyncAPI document. - A fragment-only document is parsed and validated using the reference category, such as schema, message, channel, operation, server, parameter, or binding.
An external fragment container does not need to be an AsyncAPI document or have an object root. JSON Pointer selection can traverse object or array containers, and a root scalar can be selected directly when the reference category permits that value, such as a boolean Schema Object. The selected target must satisfy the structure required by its reference category. Only the selected target is parsed; unrelated siblings in a heterogeneous raw file are not interpreted as members of the same category. A whole-file Message reference therefore selects one Message Object at the document root. A raw map containing multiple named messages is a container rather than one Message Object and must select an individual message with an explicit JSON Pointer; the parser does not bulk import or splice container members.
References discovered inside a selected raw fragment retain their concrete category and resolve same-file JSON Pointers on demand. Reference chains and cycles are deduplicated by file, pointer, category, and parser profile. A missing same-file target is reported at the nested reference in the raw fragment that owns it.
Loading is eager and deduplicated. It preserves each file's source locations and does not bundle or inline the model.
One loader call applies these additional load-wide defaults:
| Resource | Default maximum |
|---|---|
| Distinct canonical source documents | 256 |
| Unique resolved file-and-pointer targets | 4,096 |
| Acyclic external-reference depth | 64 |
| Aggregate canonical source bytes | 64 MiB |
| One native Avro or Protobuf schema asset | 20 MiB |
Canonical sources and previously processed targets count once. Cycles therefore
terminate without repeatedly consuming the budget. Native assets count toward
aggregate bytes and use bounded strict UTF-8 decoding. A limit failure is
parser.load-resource-limit-exceeded and exposes the limit, configured maximum,
observed value, and source location of the reference that caused the load.
Referenced local files are trusted build input. The loader deliberately has no project-root sandbox; adding one requires a separate configuration design.
Schema Object behavior
SchemaParser is specialized because Schema Objects are recursive and permit
shapes that ordinary AsyncAPI objects do not:
- Boolean schemas are represented as
SchemaInterface.BooleanSchemaonly when the YAML or JSON value is an actual boolean. Quoted"true"and"false"values are strings and fail structural parsing. - String
$refvalues produce schema references. - An absent
typeis valid. A presenttypeis parsed as a string, an array of strings, or an explicit null retained through field registration. The validator reports explicit null with the stableJSONSCHEMA-TYPEfinding; it is not treated as absence. Nullable schemas use a union such astype: [string, "null"]. - Recursive keywords such as
properties,items,allOf,anyOf,oneOf,not, conditional schemas, and schema-valued dependencies recurse throughSchemaParser. - The current domain model represents
itemsas one schema. Tuple validation expressed as an array of schemas is not represented inSchema.items; the parser retains the original field value and location in the model repository soSchemaValidatorcan report the unsupported form with a source-aware diagnostic. - Property dependencies are lists of strings; schema dependencies are parsed as schemas.
default,const,examples, and enum values preserve JSON-compatible plain values.defaultSetdistinguishes an absent default from an explicit null.- Known AsyncAPI Schema Object formats delegate back to ordinary schema parsing.
Known native or other Multi Format Schema values are preserved as
MultiFormatSchema; native Avro and Protobuf schemas may load supported external schema assets. - Unknown
schemaFormatvalues are rejected by the existing schema-format contract.
Schema Objects are not subject to ordinary AsyncAPI fixed-member rejection.
Unknown and unsupported keywords remain registered with their original values
and locations so SchemaValidator can issue dialect or capability findings.
Referencing a fragment from an OpenAPI-like or otherwise heterogeneous file
does not switch parser dialects: the selected fragment still follows AsyncAPI
3.0 Schema Object and JSON Schema Draft 7 behavior, while unrelated surrounding
content is ignored.
The schema parser constructs model shape. SchemaValidator remains responsible
for semantic keyword policy and combinations.
Extensions and free-form values
Extension members beginning with x- and specification fields explicitly
defined as free-form cross the typed parser boundary as JSON-compatible plain
values: Map<String, Any?>, List<Any?>, String, Number, Boolean, or
null.
Use toPlainValue() only at those boundaries. The returned value has no
SourceLocation, so the containing model and its registered field locations
remain the source of downstream diagnostics. Known structural fields must use
expect<T>(), expectObject(), or expectArray() instead of being treated as
free-form.