@usearazzo/parser reads the three languages an Arazzo document is written in. parseArazzo and parseOpenAPI turn a document into a typed SpecLynx ApiDOM tree. parseRuntimeExpression and parseCriterionCondition turn the two grammars embedded in its strings, runtime expressions and simple criterion conditions, into syntax trees. For the reasoning behind that split, and for a walk through a real document, read the Parsing Arazzo Documents guide. This page is the reference.
Installation
npm install @usearazzo/parser
The package ships ESM and CommonJS builds with TypeScript declarations, and requires Node.js 20.10 or newer. It is pre-1.0: the version documented here is an alpha, and the API may still change before the stable release.
Exports
| Export | Kind | What it is |
|---|---|---|
parseArazzo |
function | Parses an Arazzo document from a path, URL, string, or object. |
parseOpenAPI |
function | Parses an OpenAPI document the same way. |
parseRuntimeExpression |
function | Parses a bare runtime expression into an AST. |
parseCriterionCondition |
function | Parses a simple criterion condition into an AST. |
defaultParseArazzoOptions, defaultParseOpenAPIOptions |
constant | The options each document parser starts from before yours are merged in. |
defaultParseRuntimeExpressionOptions, defaultParseCriterionConditionOptions |
constant | Empty on purpose. The grammar packages supply their own defaults. |
ParseError |
class | Thrown by the two document parsers when parsing fails. |
ArazzoRuntimeExpressionParseError, ArazzoCriterionParseError |
class | Re-exported from the grammar packages. Thrown only on an internal parser error, never for invalid syntax. |
ParseArazzoOptions, ParseOpenAPIOptions |
type | PartialDeep<ApiDOMReferenceOptions>. |
ParseRuntimeExpressionOptions, ParseCriterionConditionOptions |
type | The grammar packages’ parse options. |
ParseRuntimeExpressionResult, ParseCriterionConditionResult |
type | { result, tree, stats, trace } bound to each grammar’s AST type. |
RuntimeExpressionASTNode, CriterionConditionAST |
type | The AST node types the two grammar parsers produce. |
ParseResultElement |
type | The ApiDOM element the two document parsers resolve to. |
parseArazzo
parseArazzo(source: string | Record<string, unknown>, options?: ParseArazzoOptions): Promise<ParseResultElement>
Inputs
One function, four kinds of input. The parser decides which it has been given, in this order:
- A plain object. Serialized to pretty-printed JSON with two-space indentation and parsed from that.
- A string that detects as Arazzo. Inline JSON or YAML, sniffed from the content rather than from a file extension.
- A file system path. Read from disk. Only
.json,.yaml, and.ymlfiles are accepted. - An HTTP or HTTPS URL. Fetched with a 15 second timeout and up to five redirects.
import { parseArazzo } from '@usearazzo/parser';
await parseArazzo({ arazzo: '1.0.1', info: { title: 'Pets', version: '1.0.0' }, sourceDescriptions: [], workflows: [] });
await parseArazzo('{"arazzo": "1.0.1", "info": {"title": "Pets", "version": "1.0.0"}, "sourceDescriptions": [], "workflows": []}');
await parseArazzo(`
arazzo: '1.0.1'
info:
title: Pets
version: '1.0.0'
sourceDescriptions: []
workflows: []
`);
await parseArazzo('/path/to/adopt-a-pet.arazzo.yaml');
await parseArazzo('https://example.com/adopt-a-pet.arazzo.yaml');
A string that is neither Arazzo content nor a readable location fails with a ParseError.
Options
The second argument is a deep-partial ApiDOM reference options object, merged over defaultParseArazzoOptions. The keys you will actually set live under parse.parserOpts:
| Option | Default | Effect |
|---|---|---|
strict |
true |
Only accept content the Arazzo parsers positively detect. Must be false for sourceMap and style. |
sourceMap |
false |
Record the start and end position of every element. See Source maps. |
style |
false |
Record formatting detail for round-trip output. See Style preservation. |
sourceDescriptions |
false |
Fetch and parse the documents the sourceDescriptions array points at. Accepts true or an array of names. See Source descriptions. |
sourceDescriptionsMaxDepth |
+Infinity |
How many levels of Arazzo source descriptions to follow. See Recursion and depth. |
import { parseArazzo } from '@usearazzo/parser';
const parseResult = await parseArazzo(source, {
parse: {
parserOpts: {
strict: false,
sourceMap: true,
},
},
});
Under resolve, baseURI is the one you are likely to need (next section). resolve.resolvers accepts extra ApiDOM resolvers; the parser’s own in-memory resolver is always placed ahead of them.
Both default option objects are exported, so you can inspect exactly what a call starts from:
import { defaultParseArazzoOptions, defaultParseOpenAPIOptions } from '@usearazzo/parser';
console.dir(defaultParseArazzoOptions, { depth: null });
Base URI for inline content
A document passed as an object or string has no location, so relative references inside it, such as a source description url of ./petstore.openapi.yaml, have nothing to resolve against. resolve.baseURI tells the parser where to treat the document as coming from:
import { parseArazzo } from '@usearazzo/parser';
const document = {
arazzo: '1.0.1',
info: { title: 'Pet adoption', version: '1.0.0' },
sourceDescriptions: [{ name: 'petstore', type: 'openapi', url: './petstore.openapi.yaml' }],
workflows: [],
};
const parseResult = await parseArazzo(document, {
resolve: { baseURI: '/path/to/adopt-a-pet.arazzo.json' },
parse: { parserOpts: { sourceDescriptions: true } },
});
parseResult.meta.get('retrievalURI'); // '/path/to/adopt-a-pet.arazzo.json'
parseResult.get(1).meta.get('retrievalURI'); // '/path/to/petstore.openapi.yaml'
The base URI must be absolute; a relative value is not resolved against the working directory. It is never read: the in-memory content is what gets parsed, so the path need not exist or carry a recognized extension. It is also recorded as the result’s retrievalURI.
Errors
When parsing fails for any reason, parseArazzo throws a ParseError. The message names where the input came from, and the underlying error is on cause:
import { parseArazzo, ParseError } from '@usearazzo/parser';
try {
await parseArazzo('not an arazzo document');
} catch (error) {
if (error instanceof ParseError) {
error.message; // 'Failed to parse Arazzo Document from "[inline CONTENT]"'
error.cause; // the error from the underlying parser or resolver
}
throw error;
}
The provenance in the message is the path or URL for a location, [object] for a plain object, [inline JSON] or [inline YAML] for detected inline content, and [inline CONTENT] for a string that was neither. A document that parses but is not an Arazzo document is also a ParseError, with an UnmatchedParserError as its cause.
Result
The promise resolves to a ParseResultElement:
| Accessor | Type | What it is |
|---|---|---|
api |
ArazzoSpecification1Element |
The document, as a typed tree with a getter for every field the specification defines. |
errors |
ArrayElement |
Annotations with the error class. |
warnings |
ArrayElement |
Annotations with the warning class. |
annotations |
ArrayElement |
Every annotation, whatever its class. |
isEmpty |
boolean |
True when nothing was parsed. |
meta.get('retrievalURI') |
StringElement |
Where the document was read from. Set for paths, URLs, and inline content parsed with resolve.baseURI. Absent for other inline content. |
import { parseArazzo } from '@usearazzo/parser';
import { toValue } from '@speclynx/apidom-core';
const parseResult = await parseArazzo('/path/to/adopt-a-pet.arazzo.yaml');
toValue(parseResult.api.info.title); // 'Pet adoption'
parseResult.errors.length; // 0
toValue(parseResult.meta.get('retrievalURI')); // '/path/to/adopt-a-pet.arazzo.yaml'
When source descriptions are parsed, the same element also holds their results as further top-level members. See Result structure.
Source maps
With sourceMap: true and strict: false, every element carries its position in the source. Positions are zero-based and counted in UTF-16 code units, which is what the Language Server Protocol and JavaScript string indexing use:
import { parseArazzo } from '@usearazzo/parser';
const parseResult = await parseArazzo('/path/to/adopt-a-pet.arazzo.yaml', {
parse: { parserOpts: { sourceMap: true, strict: false } },
});
const workflow = parseResult.api.workflows.get(0);
workflow.startLine; // line the element begins on
workflow.startCharacter; // column it begins at
workflow.startOffset; // offset from the start of the document
workflow.endLine;
workflow.endCharacter;
workflow.endOffset;
For a plain object, positions refer to the JSON the parser generated from it, not to anything in your program. The ApiDOM data model documentation covers source maps in full.
Style preservation
With style: true and strict: false, the parser records formatting detail on each element: YAML quoting, flow versus block style, comments, indentation, and for JSON the indentation and the raw text of numbers. toYAML and toJSON from @speclynx/apidom-core can then reproduce the original formatting:
import { parseArazzo } from '@usearazzo/parser';
import { toYAML, toJSON } from '@speclynx/apidom-core';
const fromYAML = await parseArazzo('/path/to/adopt-a-pet.arazzo.yaml', {
parse: { parserOpts: { style: true, strict: false } },
});
const yaml = toYAML(fromYAML.api, { preserveStyle: true });
const fromJSON = await parseArazzo('/path/to/adopt-a-pet.arazzo.json', {
parse: { parserOpts: { style: true, strict: false } },
});
const json = toJSON(fromJSON.api, undefined, undefined, { preserveStyle: true });
style and sourceMap are independent and can be enabled together.
Source descriptions
An Arazzo document points at the APIs its workflows call through the Source Description Object. The parser can follow those pointers and parse what it finds, so that one call gives you the workflow document and every OpenAPI or Arazzo document it depends on.
This is off by default. Following references means reading files and making network requests, so you opt in per call.
Enabling
import { parseArazzo } from '@usearazzo/parser';
const parseResult = await parseArazzo('/path/to/adopt-a-pet.arazzo.yaml', {
parse: { parserOpts: { sourceDescriptions: true } },
});
The option can also be set per parser, which matters only when you configure the JSON and YAML parsers differently:
const parseResult = await parseArazzo('/path/to/adopt-a-pet.arazzo.yaml', {
parse: {
parserOpts: {
'arazzo-json-1': { sourceDescriptions: true },
'arazzo-yaml-1': { sourceDescriptions: true },
},
},
});
Relative url values resolve against the URI the parent document was retrieved from. A parent passed as an object or inline string without resolve.baseURI has a synthetic memory:// URI, so a relative url under it cannot be retrieved. The source description then gets an error annotation saying so. Provide resolve.baseURI, use absolute file:// or https:// URLs, or declare a $self with a scheme on the parent.
Selective parsing
Pass an array of names to parse only some source descriptions:
const parseResult = await parseArazzo('/path/to/onboarding.arazzo.yaml', {
parse: { parserOpts: { sourceDescriptions: ['petstore'] } },
});
Result structure
Every document that is a direct source description of the main document is added to the main ParseResultElement as a further top-level member. The first member is always the main document. Documents discovered while recursing into an Arazzo source description are nested beneath the element that referenced them, not repeated at the top level:
ParseResultElement onboarding.arazzo.yaml
├── .api: ArazzoSpecification1Element the main document
├── ParseResultElement (petstore) direct source description
│ └── .api: OpenApi3_1Element
└── ParseResultElement (adoption) direct source description, type arazzo
├── .api: ArazzoSpecification1Element
└── ParseResultElement (petstore) reached again: shared, not reparsed
└── .meta parseResult points at the petstore element above
Each parsed document lands in the ApiDOM namespace for its type:
| Document | Namespace |
|---|---|
| Arazzo 1.x | @speclynx/apidom-ns-arazzo-1 |
| OpenAPI 2.0 | @speclynx/apidom-ns-openapi-2 |
| OpenAPI 3.0.x | @speclynx/apidom-ns-openapi-3-0 |
| OpenAPI 3.1.x | @speclynx/apidom-ns-openapi-3-1 |
Source description members carry the source-description class and their name and type in metadata:
import { parseArazzo } from '@usearazzo/parser';
import { toValue } from '@speclynx/apidom-core';
const parseResult = await parseArazzo('/path/to/adopt-a-pet.arazzo.yaml', {
parse: { parserOpts: { sourceDescriptions: true } },
});
parseResult.length; // 1 main document + N direct source descriptions
for (let i = 0; i < parseResult.length; i += 1) {
const element = parseResult.get(i);
if (element.classes.includes('source-description')) {
const name = toValue(element.meta.get('name'));
const type = toValue(element.meta.get('type'));
const api = element.api; // the parsed OpenAPI or Arazzo document
}
}
Via SourceDescriptionElement
The same ParseResultElement is also attached to the SourceDescriptionElement that referenced it, under the parseResult metadata key. Use this route when you start from a position in the sourceDescriptions array, or need to correlate a parsed document with its declaration:
import { parseArazzo } from '@usearazzo/parser';
import { toValue } from '@speclynx/apidom-core';
const parseResult = await parseArazzo('/path/to/adopt-a-pet.arazzo.yaml', {
parse: { parserOpts: { sourceDescriptions: true } },
});
const sourceDescription = parseResult.api.sourceDescriptions.get(0);
const nested = sourceDescription.meta.get('parseResult');
if (nested.errors.length === 0) {
nested.api.element; // 'openApi3_1'
toValue(nested.meta.get('retrievalURI')); // where it was fetched from
}
Recursion and depth
A source description of type arazzo is parsed with its own source descriptions followed in turn, so a tree of workflow documents comes back in one call. sourceDescriptionsMaxDepth caps how far that goes:
const parseResult = await parseArazzo('/path/to/adopt-a-pet.arazzo.yaml', {
parse: { parserOpts: { sourceDescriptions: true, sourceDescriptionsMaxDepth: 2 } },
});
The default is +Infinity. At 0 no source description is parsed and each gets an error annotation instead.
Cycles and shared documents
A true cycle, where a document references one of its own ancestors, is detected and reported as a warning annotation on the source description that closes the loop. Parsing continues; nothing recurses forever.
Reaching the same document twice through different paths is not a cycle. The common case is one OpenAPI description shared by several workflow documents. The parser parses it once. Every later source description pointing at it gets a ParseResultElement with no api of its own, an info annotation, and a parseResult metadata entry pointing at the element where the document was actually parsed. The SourceDescriptionElement’s parseResult metadata points there too:
// onboarding.arazzo.yaml references petstore.openapi.yaml and adopt-a-pet.arazzo.yaml
// adopt-a-pet.arazzo.yaml references petstore.openapi.yaml as well: shared, not a cycle
const parseResult = await parseArazzo('/path/to/onboarding.arazzo.yaml', {
parse: { parserOpts: { sourceDescriptions: true } },
});
parseResult.length; // 3: the main document, petstore, adoption
const adoption = parseResult.get(2);
const shared = adoption.api.sourceDescriptions.get(0).meta.get('parseResult');
shared === parseResult.get(1); // true: the petstore element parsed for onboarding
adoption.get(1).api; // undefined: the nested petstore entry is a pointer, not a second parse
Annotations
Problems while parsing source descriptions never throw. They become annotation elements on the source description’s own result, so the main document and every other source description still parse:
| Class | Meaning | Examples |
|---|---|---|
error |
That source description could not be parsed. | File not found, unparseable document, depth limit reached, relative URL under an inline parent. |
warning |
Parsed, with something to know. | A cycle was cut, or the declared type does not match the document found. |
info |
Nothing went wrong. | A shared document was reused instead of parsed again. |
for (let i = 0; i < parseResult.length; i += 1) {
const element = parseResult.get(i);
if (element.classes.includes('source-description')) {
const name = toValue(element.meta.get('name'));
element.errors.forEach((error) => console.error(`${name}: ${toValue(error)}`));
element.warnings.forEach((warning) => console.warn(`${name}: ${toValue(warning)}`));
}
}
parseOpenAPI
parseOpenAPI(source: string | Record<string, unknown>, options?: ParseOpenAPIOptions): Promise<ParseResultElement>
Parses an OpenAPI document on its own. Use it when you resolve source descriptions yourself, or when the OpenAPI document is what you have. It accepts the same four kinds of input as parseArazzo, takes the same options including resolve.baseURI, throws the same ParseError, and returns a ParseResultElement whose api is the element for the document’s version: openApi2, openApi3_0, or openApi3_1.
import { parseOpenAPI } from '@usearazzo/parser';
await parseOpenAPI({ openapi: '3.1.0', info: { title: 'Petstore', version: '1.0.0' }, paths: {} });
await parseOpenAPI('/path/to/petstore.openapi.yaml');
await parseOpenAPI('https://example.com/petstore.openapi.yaml');
parseRuntimeExpression
parseRuntimeExpression(expression: string, options?: ParseRuntimeExpressionOptions): ParseRuntimeExpressionResult
Parses a bare runtime expression into an AST. Bare means without the surrounding braces: $inputs.username, not {$inputs.username}. The function is synchronous and needs no document. Invalid syntax never throws; it is reported through the returned result, which makes the function safe as a pure syntax check when linting the expressions embedded in a document.
import { parseRuntimeExpression } from '@usearazzo/parser';
const { result, tree } = parseRuntimeExpression('$steps.findPet.outputs.petId');
result.success; // true
tree; // { type: 'StepsExpression', stepId: 'findPet', field: 'outputs', outputName: 'petId' }
const invalid = parseRuntimeExpression('$unknown.thing');
invalid.result.success; // false
invalid.tree; // undefined
invalid.result.maxMatched; // offset up to which parsing succeeded
The return value is { result, tree, stats, trace }. result.success says whether the whole string parsed; result.matched and result.maxMatched are offsets into the input.
It throws in two cases only:
TypeErrorwhenexpressionis not a string.ArazzoRuntimeExpressionParseErroron an unexpected internal error, which is distinct from invalid syntax.
import { parseRuntimeExpression, ArazzoRuntimeExpressionParseError } from '@usearazzo/parser';
try {
parseRuntimeExpression(expression);
} catch (error) {
if (error instanceof ArazzoRuntimeExpressionParseError) {
console.error('Unexpected parser error:', error.cause);
}
throw error;
}
The function wraps @swaggerexpert/arazzo-runtime-expression, and RuntimeExpressionASTNode and ParseRuntimeExpressionResult are that package’s types under this package’s names. Splitting a {expression} template into literal and expression spans, or interpolating one, stays with that package’s extract and interpolate functions.
parseCriterionCondition
parseCriterionCondition(condition: string, options?: ParseCriterionConditionOptions): ParseCriterionConditionResult
Parses the Criterion Object’s simple condition grammar into an AST. It behaves like parseRuntimeExpression: synchronous, no document, invalid syntax reported through result rather than thrown. Each runtime expression operand inside the condition is parsed too, and its sub-AST rides on the RuntimeExpression node that holds it.
import { parseCriterionCondition } from '@usearazzo/parser';
const { result, tree } = parseCriterionCondition('$statusCode == 200');
result.success; // true
tree.type; // 'BinaryExpression'
const invalid = parseCriterionCondition('$statusCode ===');
invalid.result.success; // false
invalid.tree; // undefined
It throws a TypeError for a non-string condition and an ArazzoCriterionParseError on an unexpected internal error. The function wraps @swaggerexpert/arazzo-criterion; CriterionConditionAST and ParseCriterionConditionResult are its types.
JSONPath and JSON Pointer, which can appear inside expressions, are general-purpose syntaxes with their own ecosystems and are out of scope here. Use @swaggerexpert/jsonpath or a JSON Pointer library directly for those.
Working with the tree
The document parsers return ApiDOM, so every ApiDOM tool applies. Two packages cover most needs.
@speclynx/apidom-core converts and serializes:
import { parseArazzo } from '@usearazzo/parser';
import { toValue, toJSON, toYAML, sexprs } from '@speclynx/apidom-core';
import { cloneDeep, cloneShallow } from '@speclynx/apidom-datamodel';
const { api } = await parseArazzo(source);
const object = toValue(api); // plain JavaScript value
const json = toJSON(api); // JSON string
const yaml = toYAML(api); // YAML string
const deep = cloneDeep(api);
const shallow = cloneShallow(api);
const sexpr = sexprs(api); // S-expression dump, handy when debugging
@speclynx/apidom-traverse walks the tree with visitors keyed by element type:
import { parseArazzo } from '@usearazzo/parser';
import { traverse } from '@speclynx/apidom-traverse';
const { api } = await parseArazzo(source);
const steps = [];
traverse(api, {
StepElement(path) {
steps.push(path.node);
if (steps.length >= 10) path.stop();
},
});
The SpecLynx ApiDOM repository documents the rest.
Supported versions
Arazzo documents:
OpenAPI documents, as source descriptions or through parseOpenAPI:
Both JSON and YAML are accepted for every version. The format is detected from the content, not the file extension.