Skip to content

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.json was recorded from the parser this replaced and is asserted against on every build - it is simply read by engine/src/core/import_document.cpp now, behind POST /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

detect(parsed) => parsed._type === "export" && parsed.__export_format === 4

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():

  1. Index by parent. Build byParent: Map<parentId, Resource[]>, grouping every resource under its parentId (resources with no parentId use the empty-string key ""). This is the single adjacency index used for the whole tree walk.
  2. Find roots. Filter resources for _type === "workspace". Each workspace becomes one root CollectionDraft. Multiple workspaces ⇒ multiple root collections in ImportResult.collections.
  3. Recursive build. buildCollection(node, isWorkspace) walks byParent.get(node._id) and dispatches each child by _type:
  4. request_group → increments folderCount, recurses via buildCollection(child, false), pushed to children.
  5. requestbuildRequest(child), pushed to requests.
  6. other handled _types (gRPC / WebSocket / spec / tests) → counted in skippedCounts (see Resource type handling).
  7. any unlisted _type (including environment) → silently ignored by the collection walk; environments are processed separately.
  8. buildRequest(r) increments requestCount and produces a RequestDraft.

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 to Bearer. Vayu's bearer mode always writes Bearer, so a different scheme is preserved as an explicit Authorization header through the apikey mode - the engine writes an apikey header 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 through normalize_template_vars and the composed value is trimmed, so an empty token yields just the scheme.
  • OAuth2 tokenPrefix maps to OAuth2Config.headerPrefix (absent or empty → "Bearer"), matching map_postman_oauth2's headerPrefix. Vayu executes OAuth2, so an unread prefix would send Bearer and 401 against a server expecting another scheme.
  • disabled takes precedence over type. If authentication.disabled === true, the result is { mode: "none" } regardless of type. If authentication is missing or has no type (and is not disabled), the result is { mode: "inherit" }.
  • oauth2 is mapped to an executable OAuth2Config (map_insomnia_oauth2) and does not count. digest/ntlm/iam are stored as opaque config bags (the auth object with type and disabled removed) and are not executed by Vayu - each occurrence increments meta.nonExecutableAuth.
  • The same insomniaAuth is called for collection-level auth (workspace/request_group), sharing the same authCtx. So a non-executable auth on a workspace or folder also counts toward nonExecutableAuth. For collections, an inherit result 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:

  1. Base environments: environment resources whose parentId is the workspace _id.
  2. Sub-environments: environment resources whose parentId is a base environment _id.

Flattening per base:

  • No sub-envs: emit one EnvironmentDraft from the base's data. Name = base.name ?? workspace.name ?? "Environment".
  • Has sub-envs: emit one EnvironmentDraft per 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, every preRequestScript/postRequestScript is forced to "". When true, they come from preRequestScript and afterResponseScript, on requests and on workspaces/request_groups (see the caveat in the collection field table).
  • importEnvironments - when false, the entire environment pass is skipped; environments is [] and environmentCount is 0. Workspace-level environment data still populates the root collection's variables independently 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-data file params that name no path, are dropped and counted as file_body in meta.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) increments meta.nonExecutableAuth. oauth2 is 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.