Insomnia v4¶
Parses an Insomnia "Export v4" JSON document into the Vayu draft model. Insomnia exports are a single flat array of typed resources joined by _id/parentId; the parser reconstructs the workspace → request_group → request tree and emits one root collection per workspace.
- Source:
engine/src/core/import_document.cpp - Exports:
The parse moved engine-side (issue #877). Every rule on this page is the same rule it always was - the corpus in
engine/tests/fixtures/import-conformance.jsonwas recorded from the parser this replaced and is asserted against on every build - it is simply read byengine/src/core/import_document.cppnow, behindPOST /import/parse, rather than in the renderer. Module names in C++ style below name that file's functions; the app holds no parser.
| Class | formatName |
formatKey |
|---|---|---|
InsomniaV4Parser |
Insomnia Export v4 |
insomnia-v4 |
Implements ImportParser (detect + parse) from ./types. meta.format is set to this.formatName ("Insomnia Export v4").
Detection¶
Both conditions must hold: the top-level object must have _type === "export" and __export_format === 4 (numeric). No other format versions are accepted by this parser.
Structure & tree reconstruction¶
An Insomnia v4 export has the shape { _type: "export", __export_format: 4, resources: Resource[] }. Each resource carries _id, _type, an optional parentId, and type-specific fields. There is no nesting in the JSON - hierarchy is expressed entirely through parentId references.
Reconstruction steps in parse():
- Index by parent. Build
byParent: Map<parentId, Resource[]>, grouping every resource under itsparentId(resources with noparentIduse the empty-string key""). This is the single adjacency index used for the whole tree walk. - Find roots. Filter
resourcesfor_type === "workspace". Each workspace becomes one rootCollectionDraft. Multiple workspaces ⇒ multiple root collections inImportResult.collections. - Recursive build.
buildCollection(node, isWorkspace)walksbyParent.get(node._id)and dispatches each child by_type: request_group→ incrementsfolderCount, recurses viabuildCollection(child, false), pushed tochildren.request→buildRequest(child), pushed torequests.- other handled
_types (gRPC / WebSocket / spec / tests) → counted inskippedCounts(see Resource type handling). - any unlisted
_type(includingenvironment) → silently ignored by the collection walk; environments are processed separately. buildRequest(r)incrementsrequestCountand produces aRequestDraft.
Key internal functions: parse (entry), buildCollection, buildRequest, insomniaAuth, insomniaBody, to_env_vars. Counters (requestCount, folderCount, authCtx.nonExec) are closures mutated during the walk.
Resource type handling¶
Insomnia _type |
Vayu outcome | Notes |
|---|---|---|
workspace |
Root CollectionDraft |
One per workspace. isWorkspace = true. |
request_group |
Nested CollectionDraft (folder) |
Increments folderCount. |
request |
RequestDraft |
Increments requestCount. |
environment |
EnvironmentDraft (flattened) |
Processed in a separate pass, not in the collection walk. Gated by importEnvironments. See Environments. |
grpc_request |
Dropped | meta.skipped kind "grpc". |
websocket_request |
Dropped | meta.skipped kind "websocket". |
api_spec |
Dropped | meta.skipped kind "api_spec". |
unit_test |
Dropped | meta.skipped kind "unit_test". |
unit_test_suite |
Dropped | meta.skipped kind "unit_test" (folded into the same unit_test bucket). |
anything else (e.g. cookie_jar, proto_file, request_meta, environment outside its pass) |
Dropped silently | Not counted in meta.skipped. |
Skip counting nuance. Only the five _types listed above (grpc_request, websocket_request, api_spec, unit_test, unit_test_suite) are tallied in skippedCounts by the tree walk, and only when they appear as a direct child of a workspace or request_group. A dropped resource parented under something else (or any other _type) does not increment meta.skipped. When emitting SkippedItem[], the raw keys are remapped: grpc_request → "grpc", websocket_request → "websocket", unit_test_suite → "unit_test"; unit_test and api_spec pass through unchanged. Because unit_test and unit_test_suite both map to "unit_test" but are aggregated by their raw key first, an export containing both can yield two separate SkippedItem entries with kind: "unit_test".
A sixth kind, file_body, does not come from a resource _type at all: it counts bodies Vayu cannot store - binary bodies, and a multipart/form-data file param that names no path (a file part with a path imports as a file row; see Body mapping) - and is appended once, after the walk, when the count is non-zero.
A seventh kind, path_variables, is informational rather than lossy (see the pathParameters[] row above): it counts once per request whose URL carried a :key → {{key}} substitution, and is appended per collection/folder node once that node's own subtree is walked.
Field mapping¶
Collection (workspace / request_group)¶
Both build through buildCollection. Differences are gated by isWorkspace.
| Insomnia field | Vayu CollectionDraft field |
Notes |
|---|---|---|
name |
name |
Falls back to "Imported". |
description |
description |
Falls back to "". |
environment (object, workspace only) |
variables |
toEnvVars(node.environment ?? {}) for workspaces; request_groups always get variables: {} (their inline environment, if any, is not read). |
authentication |
auth |
Mapped via insomniaAuth. Collections may not be inherit: a resulting inherit is coerced to { mode: "none" }. |
preRequestScript / afterResponseScript |
preRequestScript / postRequestScript |
Only if opts.importScripts; else "". Folder-level scripts are a real Insomnia 9.3+ feature, and its v4 export writes model fields verbatim, so these are the same key names the request path reads. The names are not pinned by a live 9.x export in this repo - an export that spells them differently reads as absent, i.e. the empty strings this parser produced before. |
| (reconstructed children) | children / requests |
Built from byParent. |
Request¶
Built by buildRequest.
| Insomnia field | Vayu RequestDraft field |
Notes |
|---|---|---|
name |
name |
Falls back to "Untitled". |
description |
description |
Falls back to "". |
method |
method |
Upper-cased; restricted to GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS, otherwise defaults to GET. |
url |
url |
normalize_template_vars(as_string(url)) - taken verbatim, query string and all - then run through the same :key → {{key}} rewrite pathParameters[] (below) declares; the query string (and any fragment) is split off first and put back untouched, so a : inside a query value (a timestamp, a scoped tag, a port in a redirect URL) is never read as a path-variable token. The parse then appends the enabled parameters[] to it (see The url/params invariant), which is what Insomnia itself does with its two query sources on send: a URL written https://x/y?a=1 beside a b=2 parameter stores as https://x/y?a=1&b=2. |
parameters[] ({name,value,disabled,description}) |
params |
map_key_values: name → key, disabled !== true → enabled. A string description is forwarded (any other type is ignored). Rows without a key are dropped. Enabled rows also join the url, per the row above. |
pathParameters[] ({name,value}) |
url template + variables |
The same :key path-segment rewrite Postman's url.variable[] gets (see the Postman doc, "Path variables"): the URL's path is walked one /-delimited segment at a time, and a segment that is exactly :key for a declared key is rewritten to {{key}} - every occurrence, not just the first, so /:id/copies/:id rewrites both - with the entry's value recorded once per key. Recorded keys are merged into the nearest enclosing collection's variables (the request's own workspace or request_group, not necessarily the root) once that node's own subtree is walked, skipping a key already declared there explicitly. Counted as path_variables, informational rather than lossy - the value survives as a variable. |
headers[] ({name,value,disabled,description}) |
headers |
Same mapping as params. |
settingFollowRedirects ("global" \| "on" \| "off") |
followRedirects |
"off" → false, "on" → true; "global" (Insomnia's default, meaning "use the app setting", which follows redirects) and an absent field leave the draft field absent, so the engine default (true) applies. Insomnia has no per-request redirect limit, so maxRedirects is never imported. |
body |
body |
Via insomniaBody. See Body mapping. |
authentication |
auth |
Via insomniaAuth. See Auth mapping. |
preRequestScript |
preRequestScript |
Only if opts.importScripts; else "". |
afterResponseScript |
postRequestScript |
Only if opts.importScripts; else "". Note the source field is afterResponseScript. |
_id |
- | Not propagated to the draft id; used only for tree reconstruction. |
map_key_values runs normalize_template_vars over every value and preserves duplicate keys and order.
Template normalization¶
All variable-bearing strings are run through normalize_template_vars (path_template.cpp). It converts foreign template syntax to Vayu's {{var}} form:
{{ x }}and{{ _.x }}(identifier only, whitespace tolerant) →{{x}}. The leading_.namespace Insomnia uses is stripped.- Single-brace
{x}is left alone here. The rewrite exists for OpenAPI path templates and is opt-in per format (normalize_path_templates(path)); Insomnia templates with{{x}}only, so{x}is literal text. - Left verbatim: Nunjucks tags
{% ... %}and filtered expressions{{ x | filter }}. The simple-var regex requires an identifier-only body ([\w.$-]+), so a|filter never matches and is passed through unchanged. Vayu has no equivalent, so these remain as literal text.
Applied to: request url; every params and headers value (inside map_key_values); JSON/text/graphql body content and urlencoded/form-data field values; every auth token/username/password/key/value string; and every environment value (inside to_env_vars).
Body mapping¶
insomniaBody keys off body.mimeType, stripping any ;charset=... suffix and trimming.
Insomnia body.mimeType |
Vayu RequestBody |
Notes |
|---|---|---|
application/json |
{ mode: "json", content } |
content = normalize_template_vars(body.text). |
text/plain |
{ mode: "text", content } |
content = normalize_template_vars(body.text). |
application/graphql |
{ mode: "graphql", content } |
content = toGraphQLEnvelope(normalize_template_vars(body.text)) - a bare query document is wrapped into {query}, an envelope passes through (see below). The request also gains a Content-Type. |
application/xml, text/xml |
{ mode: "xml", content } |
content = normalize_template_vars(body.text). The request also gains Content-Type: application/xml, and a declared one (application/soap+xml) wins - both through the same with_required_content_type rule GraphQL uses. |
application/x-www-form-urlencoded |
{ mode: "x-www-form-urlencoded", fields } |
body.params[] → map_key_values (name → key, disabled honored). |
multipart/form-data |
{ mode: "form-data", fields } |
body.params[] through map_key_values, except that a type: "file" param becomes a file row: Insomnia keeps the path in param.fileName, which becomes src (and its basename the declared fileName), and the row is marked unresolved since the path is from the exporting machine. Only a file param naming no path increments the file_body count in meta.skipped. |
anything else carrying text (application/yaml, text/csv, Insomnia's "Other") |
{ mode: "text", content } |
content = normalize_template_vars(body.text). Reserved for a non-empty string body.text; the sibling Postman parser's raw_body() fallback behaves the same way. No JSON sniffing happens here - Insomnia states the mime. |
anything else with no text (binary / file-only: body.fileName set) |
{ mode: "none" } |
Counted as file_body in meta.skipped - Vayu has no file body mode, and the file lives outside the export anyway. |
| missing or empty body | { mode: "none" } |
Nothing was lost, so nothing is counted. |
GraphQL bodies (toGraphQLEnvelope): Insomnia writes application/graphql for both shapes its editor produces - the GraphQL-over-HTTP envelope ({query, variables}) and, for a hand-written or older request, the bare query document. A bare document stored verbatim went on the wire as the whole HTTP body: not JSON, so a GraphQL server reads no query at all. Nothing showed it, because the query pane's raw-string fallback renders a bare document exactly as it renders a healthy one. It is normalized at import - the one place the shape is known to be GraphQL - and an envelope is returned untouched, so a mislabelled JSON envelope is never double-wrapped.
GraphQL Content-Type (with_required_content_type in import_document.cpp): the envelope is JSON, so the request needs Content-Type: application/json; the request builder adds it only on an interactive mode switch, which an import never performs. Written at import through the same contentTypeToAdd rule, so a Content-Type the export declares wins and a disabled row does not count as declaring one.
Auth mapping¶
insomniaAuth(authentication, ctx):
Insomnia authentication.type |
Vayu RequestAuth |
nonExecutableAuth? |
|---|---|---|
(absent / no type) |
{ mode: "inherit" } |
no |
any type with disabled === true |
{ mode: "none" } |
no |
bearer (no prefix, or a prefix that is Bearer in any case) |
{ mode: "bearer", token } |
no |
bearer with another prefix (e.g. Token, JWT) |
{ mode: "apikey", key: "Authorization", value: "<prefix> <token>", in: "header" } |
no |
basic |
{ mode: "basic", username, password } |
no |
apikey |
{ mode: "apikey", key, value, in } - in is "query" when addTo === "queryParams", else "header" |
no |
oauth2 |
{ mode: "oauth2", config: OAuth2Config } via map_insomnia_oauth2 - executable |
no |
digest |
{ mode: "digest", config } |
yes |
ntlm |
{ mode: "ntlm", config } |
yes |
iam |
{ mode: "aws", config } - Insomnia names AWS IAM "iam"; Vayu stores it as the aws config bag |
yes |
| any other / unrecognized type | { mode: "inherit" } |
no |
Notes:
- Bearer PREFIX. Insomnia sends
Authorization: <prefix> <token>, defaulting an empty PREFIX toBearer. Vayu's bearer mode always writesBearer, so a different scheme is preserved as an explicitAuthorizationheader through theapikeymode - the engine writes anapikeyheader value verbatim, so the wire bytes match what Insomnia sent. A prefix differing only in case (bearer) keeps the native bearer mode: HTTP auth schemes are case-insensitive (RFC 7235 §2.1). The prefix itself goes throughnormalize_template_varsand the composed value is trimmed, so an empty token yields just the scheme. - OAuth2
tokenPrefixmaps toOAuth2Config.headerPrefix(absent or empty →"Bearer"), matchingmap_postman_oauth2'sheaderPrefix. Vayu executes OAuth2, so an unread prefix would sendBearerand 401 against a server expecting another scheme. disabledtakes precedence overtype. Ifauthentication.disabled === true, the result is{ mode: "none" }regardless oftype. Ifauthenticationis missing or has notype(and is not disabled), the result is{ mode: "inherit" }.oauth2is mapped to an executableOAuth2Config(map_insomnia_oauth2) and does not count.digest/ntlm/iamare stored as opaqueconfigbags (the auth object withtypeanddisabledremoved) and are not executed by Vayu - each occurrence incrementsmeta.nonExecutableAuth.- The same
insomniaAuthis called for collection-level auth (workspace/request_group), sharing the sameauthCtx. So a non-executable auth on a workspace or folder also counts towardnonExecutableAuth. For collections, aninheritresult is coerced to{ mode: "none" }(collections can never inherit).
Environments¶
Environments are imported only when opts.importEnvironments is true. They are reconstructed and flattened per workspace:
- Base environments:
environmentresources whoseparentIdis the workspace_id. - Sub-environments:
environmentresources whoseparentIdis a base environment_id.
Flattening per base:
- No sub-envs: emit one
EnvironmentDraftfrom the base'sdata. Name =base.name ?? workspace.name ?? "Environment". - Has sub-envs: emit one
EnvironmentDraftper sub-env, with variables{ ...baseVars, ...subVars }(sub-env values override base on key collision). Name =sub.name ?? "Environment". The standalone base environment is not emitted when sub-envs exist - its values survive only merged into each sub-env.
Each variable is produced by to_env_vars: keys come straight from the env data object; values are normalize_template_vars(as_string(v)) (objects/arrays are JSON-stringified, numbers/booleans coerced to strings), and every variable is { value, enabled: true }. secret is never set. meta.environmentCount equals the number of emitted EnvironmentDrafts.
Options & lossy behavior¶
ImportOptions:
importScripts- when false, everypreRequestScript/postRequestScriptis forced to"". When true, they come frompreRequestScriptandafterResponseScript, on requests and on workspaces/request_groups (see the caveat in the collection field table).importEnvironments- when false, the entire environment pass is skipped;environmentsis[]andenvironmentCountis0. Workspace-levelenvironmentdata still populates the root collection'svariablesindependently of this flag.
Lossy / dropped, summary:
- gRPC, WebSocket, API spec, unit test, and unit-test-suite resources are dropped and counted in
meta.skipped(only when encountered as direct children of a workspace/request_group during the tree walk; see counting nuance above). - Binary bodies, and
multipart/form-datafile params that name no path, are dropped and counted asfile_bodyinmeta.skipped. A file param with a path imports as an unresolved file row. - Non-executable auth types (
digest,ntlm,iam→aws) are stored but not run; each occurrence (request or collection level) incrementsmeta.nonExecutableAuth.oauth2is executable and excluded. - Nunjucks tags and filtered template expressions are preserved as literal text.
- request_group inline environments and resource
_ids are not carried into the draft model. - No per-request redirect limit is imported (Insomnia keeps that as an app-wide setting), and no other
setting*field is read - cookie handling, URL encoding and timeline size have no Vayu equivalent.
meta: { format: "Insomnia Export v4", requestCount, folderCount, environmentCount, globalCount: 0, skipped, nonExecutableAuth } (no fileName set by the parser itself; globals is always {} - Insomnia has no globals scope).
Malformed exports¶
Insomnia itself only emits arrays and string mime types, so the shapes below mean a hand-edited or script-mangled file. Each throws Error("Malformed Insomnia export: <detail>"), which ImportModal shows verbatim - previously they surfaced as a raw TypeError ("Cannot read properties of undefined (reading 'map')") or, for a cycle, a RangeError. Nothing is persisted either way: the parse happens before any write.
| Input | Detail |
|---|---|
resources present but not an array |
`resources` must be an array |
a resources entry that is not an object (e.g. null) |
`resources[<i>]` must be an object |
a request's parameters / headers present but not an array |
request "<name>": \parameters` must be an array` |
body.params present but not an array |
`body.params` must be an array |
body present but not an object |
a request `body` must be an object |
body.mimeType present but not a string |
`body.mimeType` must be a string |
a parentId loop (only reachable via a duplicated _id) |
resource "<id>" appears twice in the folder tree |
An absent field is not an error anywhere in that table - only a present-but-wrong shape is.
Shared helpers used¶
| Helper | Source | Used for |
|---|---|---|
as_string |
import_document.cpp |
Coerce any scalar/object to its string form (objects → JSON.stringify). |
map_key_values |
import_document.cpp |
Map {name,value,disabled} arrays → KeyValueEntry[] (filters keyless rows, normalizes values, derives enabled from disabled). |
normalize_template_vars |
path_template.cpp |
Template syntax normalization (see Template normalization). |
See ./README.md for the full shared-helper reference.