Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## [Unreleased]

## [v6.3.1] - 2026-07-23

## [v6.3.0] - 2026-07-07

## [v6.2.0] - 2026-06-29
Expand Down Expand Up @@ -697,7 +699,9 @@ Newer releases follow the [Keep a Changelog](https://keepachangelog.com/en/1.0.0

- Base release

[Unreleased]: https://github.com/postmanlabs/openapi-to-postman/compare/v6.3.0...HEAD
[Unreleased]: https://github.com/postmanlabs/openapi-to-postman/compare/v6.3.1...HEAD

[v6.3.1]: https://github.com/postmanlabs/openapi-to-postman/compare/v6.3.0...v6.3.1

[v6.3.0]: https://github.com/postmanlabs/openapi-to-postman/compare/v6.2.0...v6.3.0

Expand Down
183 changes: 151 additions & 32 deletions libV2/CollectionGeneration/schemaUtils.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,11 @@ export function syncCollection(
currentResponsesByCode[response.code].push(response);
});

// The spec carries a single request example (the live body). It is applied to the originalRequest
// of only the first response processed overall (isFirstSyncedResponse); every subsequent response
// preserves its existing originalRequest body (see mergeResponseData), so a request change in the
// spec updates just that first response rather than every response's originalRequest.
// The spec carries a single live request (body + primary parameter values). It is applied to the
// originalRequest of only the first response processed overall (isFirstSyncedResponse); every
// subsequent response preserves its existing originalRequest request-side data — body, url
// (query/path) and headers (see mergeResponseData) — so a request/parameter change in the spec
// updates just that first response rather than every response's originalRequest.
let isFirstSyncedResponse = true;

item.responses.each((response) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Response, ResponseDefinition } from 'postman-collection';
import { HeaderDefinition, Response, ResponseDefinition } from 'postman-collection';

import { mergeRequestAndResponseBodyRaw } from './BodyMerger';
import { mergeRequestAndResponseHeaders } from './HeaderMerger';
Expand All @@ -7,24 +7,78 @@ import { mergeRequestData } from './RequestMerger';
import { SyncOptions } from '../../shared';
import { attachImplicitHeaders } from '../header';

/**
* Preserve an existing saved response's header PARAMETER values while keeping any
* implicit/generated headers (e.g. Content-Type, Accept) produced from the spec. Used when
* preserving a non-first response's originalRequest on multi-example sync: for a shared key the
* spec-generated header metadata (name casing, description, schema-derived flags) is kept and only
* the existing header's `value`/`disabled` are copied over; generated target-only headers are
* retained, and header params that only exist on the source are appended.
*
* Header keys are matched case-insensitively (HTTP header names are case-insensitive).
* @param {HeaderDefinition[] | undefined} targetHeaders - Spec-generated headers (base)
* @param {HeaderDefinition[] | undefined} sourceHeaders - Existing saved response headers to preserve
* @returns {HeaderDefinition[] | undefined} Merged header list
*/
function preserveHeaderParams(
targetHeaders: HeaderDefinition[] | undefined,
sourceHeaders: HeaderDefinition[] | undefined
): HeaderDefinition[] | undefined {
if (!Array.isArray(sourceHeaders)) {
return targetHeaders;
}

const result: HeaderDefinition[] = Array.isArray(targetHeaders) ? [...targetHeaders] : [],
indexByLowerKey = new Map<string, number>();

result.forEach((header, index) => {
if (typeof header?.key === 'string') {
indexByLowerKey.set(header.key.toLowerCase(), index);
}
});

sourceHeaders.forEach((sourceHeader) => {
if (typeof sourceHeader?.key !== 'string') {
return;
}

const lowerKey = sourceHeader.key.toLowerCase();

if (indexByLowerKey.has(lowerKey)) {
// Keep the spec-generated header (name casing + metadata); copy only the existing header's
// value/disabled state onto it.
const index = indexByLowerKey.get(lowerKey) as number;

result[index] = { ...result[index], value: sourceHeader.value, disabled: sourceHeader.disabled };
}
else {
indexByLowerKey.set(lowerKey, result.length);
result.push(sourceHeader);
}
});

return result;
}

/**
* Merges a single response from target with a corresponding response from current.
* Returns the merged response definition.
* @param {Response} targetResponse - Response from the target request
* @param {Response } sourceResponse - Response from the source request
* @param {SyncOptions} syncOptions - Options to control what should be synced
* @param {boolean} preserveOriginalRequestBody - When true (and example syncing is enabled), keep
* the existing response's `originalRequest.body` instead of overwriting it with the spec's request.
* The spec carries a single request example (the live body) that maps to the first saved response;
* set this for every response after the first so a request change in the spec doesn't overwrite
* every response's originalRequest. Defaults to false (first response takes the spec request).
* @param {boolean} preserveOriginalRequest - When true (and example syncing is enabled), keep the
* existing response's `originalRequest` request-side data (body, url query/path variables and
* headers) instead of overwriting it with the spec's request. The spec carries a single live
* request (body + parameter values) that maps to the first saved response; set this for every
* response after the first so a request/parameter change in the spec doesn't overwrite every
* response's originalRequest. Defaults to false (first response takes the spec request).
* @returns {ResponseDefinition} Merged response definition
*/
export function mergeResponseData(
targetResponse: Response,
sourceResponse: Response,
syncOptions: SyncOptions,
preserveOriginalRequestBody = false
preserveOriginalRequest = false
): ResponseDefinition {
const targetRes: ResponseDefinition = targetResponse.toJSON(),
sourceRes: ResponseDefinition = sourceResponse.toJSON(),
Expand All @@ -34,15 +88,32 @@ export function mergeResponseData(
targetRes.originalRequest = mergeRequestData(targetRes.originalRequest, sourceRes.originalRequest, syncOptions);

/*
* The spec carries a single request example (the live request body), which maps to the first
* saved response. For the remaining responses, preserve their existing request body so that
* editing the request in the spec doesn't overwrite every response's originalRequest on sync-back.
* The spec carries a single live request (its body plus the primary parameter values), which
* maps to the first saved response. For the remaining responses, preserve their existing
* request-side data — body, url (query params + path variables) and headers — so that editing
* the request or a parameter in the spec doesn't overwrite every response's originalRequest on
* sync-back. Each component is deleted when the source had none, mirroring the body rule.
* Only applies when example syncing is enabled (the multi-example flow).
*/
if (preserveOriginalRequestBody && shouldSyncExamples) {
if (sourceRes.originalRequest.body === undefined) { delete targetRes.originalRequest.body; }
else { targetRes.originalRequest.body = sourceRes.originalRequest.body; }
}
if (preserveOriginalRequest && shouldSyncExamples) {
// Cast for the delete-when-source-had-none branches (url/body are not always optional in the type).
const targetOriginalRequest = targetRes.originalRequest as unknown as Record<string, unknown>;

// Body: preserve the existing example's body wholesale (delete when the source had none).
if (sourceRes.originalRequest.body === undefined) { delete targetOriginalRequest.body; }
else { targetRes.originalRequest.body = sourceRes.originalRequest.body; }

// URL: preserve the existing example's url (query params + path variable values) wholesale.
if (sourceRes.originalRequest.url === undefined) { delete targetOriginalRequest.url; }
else { targetRes.originalRequest.url = sourceRes.originalRequest.url; }

// Headers: preserve the existing example's header PARAMETER values while keeping any
// implicit/generated headers (e.g. Content-Type, Accept) the spec produced.
targetRes.originalRequest.header = preserveHeaderParams(
targetRes.originalRequest.header as HeaderDefinition[] | undefined,
sourceRes.originalRequest.header as HeaderDefinition[] | undefined
);
}
}
// Attach implicit headers from the source response to the target response
// if they are not present in the target response
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "openapi-to-postmanv2",
"version": "6.3.0",
"version": "6.3.1",
"description": "Convert a given OpenAPI specification to Postman Collection v2.0",
"homepage": "https://github.com/postmanlabs/openapi-to-postman",
"bugs": "https://github.com/postmanlabs/openapi-to-postman/issues",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ module.exports = {
shouldSyncRequestToFirstResponseOnly: require('./shouldSyncRequestToFirstResponseOnly'),
shouldPreserveOtherSameCodeResponsesOnRequestSync: require('./shouldPreserveOtherSameCodeResponsesOnRequestSync'),
shouldPairSameCodeExamplesPositionallyOnSync: require('./shouldPairSameCodeExamplesPositionallyOnSync'),
shouldPairParameterExamplesByMatchingKey: require('./shouldPairParameterExamplesByMatchingKey'),
shouldDeleteOrphanRequestsWhenEnabled: require('./shouldDeleteOrphanRequestsWhenEnabled'),
// Multi-file specification test cases
multiFileSpecs: require('./multiFileSpecs')
Expand Down
Loading
Loading