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
7 changes: 7 additions & 0 deletions .changeset/kind-terms-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@hey-api/json-schema-ref-parser": patch
"@hey-api/openapi-ts": patch
"@hey-api/shared": patch
---

**parser**: fix: avoid encoding url unsafe characters
Comment thread
mrlubos marked this conversation as resolved.
175 changes: 175 additions & 0 deletions packages/json-schema-ref-parser/src/__tests__/bundle.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

Expand All @@ -11,6 +12,10 @@ const __dirname = path.dirname(__filename);
const getSnapshotsPath = () => path.join(__dirname, '__snapshots__');
const getTempSnapshotsPath = () => path.join(__dirname, '.gen', 'snapshots');

const writeJsonFile = (filePath: string, value: unknown) => {
fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
};

/**
* Helper function to compare a bundled schema with a snapshot file.
* Handles writing the schema to a temp file and comparing with the snapshot.
Expand Down Expand Up @@ -46,6 +51,176 @@ describe('bundle', () => {
await expectBundledSchemaToMatchSnapshot(schema, 'circular-ref-with-description.json');
});

it('emits decoded internal refs for generic component names', async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'json-schema-ref-parser-'));

try {
const rootPath = path.join(tempDir, 'root.json');

writeJsonFile(rootPath, {
components: {
schemas: {
ClientResponse: {
properties: {
id: {
type: 'string',
},
},
type: 'object',
},
'PaginatedListItems<ClientResponse>': {
properties: {
items: {
items: {
$ref: '#/components/schemas/ClientResponse',
},
type: 'array',
},
},
type: 'object',
},
},
},
info: {
title: 'Test API',
version: '1.0.0',
},
openapi: '3.0.0',
paths: {
'/clients': {
get: {
responses: {
'200': {
content: {
'application/json': {
schema: {
$ref: '#/components/schemas/PaginatedListItems<ClientResponse>',
},
},
},
description: 'ok',
},
},
},
},
},
});

const refParser = new $RefParser();
const schema = (await refParser.bundle({ pathOrUrlOrSchema: rootPath })) as any;

expect(
schema.paths['/clients'].get.responses['200'].content['application/json'].schema.$ref,
).toBe('#/components/schemas/PaginatedListItems<ClientResponse>');

const bundledJson = JSON.stringify(schema);
expect(bundledJson).not.toContain('PaginatedListItems%3CClientResponse%3E');
} finally {
fs.rmSync(tempDir, { force: true, recursive: true });
}
});

it('emits decoded refs for external schemas with generic and unicode names', async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'json-schema-ref-parser-'));

try {
const externalPath = path.join(tempDir, 'external.json');
const rootPath = path.join(tempDir, 'root.json');

writeJsonFile(externalPath, {
components: {
schemas: {
'PaginatedList<ClientItem>': {
description: 'generic schema',
properties: {
next: {
$ref: '#/components/schemas/PaginatedList<ClientItem>',
},
},
type: 'object',
},
Überschrift: {
description: 'unicode schema',
properties: {
next: {
$ref: '#/components/schemas/Überschrift',
},
},
type: 'object',
},
},
},
});

writeJsonFile(rootPath, {
info: {
title: 'Test API',
version: '1.0.0',
},
openapi: '3.0.0',
paths: {
'/generic': {
get: {
responses: {
'200': {
content: {
'application/json': {
schema: {
$ref: 'external.json#/components/schemas/PaginatedList<ClientItem>',
},
},
},
description: 'ok',
},
},
},
},
'/unicode': {
get: {
responses: {
'200': {
content: {
'application/json': {
schema: {
$ref: 'external.json#/components/schemas/Überschrift',
},
},
},
description: 'ok',
},
},
},
},
},
});

const refParser = new $RefParser();
const schema = (await refParser.bundle({ pathOrUrlOrSchema: rootPath })) as any;
const schemas = schema.components.schemas as Record<string, any>;

const findSchemaByDescription = (description: string) =>
Object.entries(schemas).find(([, value]) => value.description === description);

const genericSchema = findSchemaByDescription('generic schema');
const unicodeSchema = findSchemaByDescription('unicode schema');

expect(genericSchema).toBeDefined();
expect(unicodeSchema).toBeDefined();

const [genericName, genericValue] = genericSchema!;
const [unicodeName, unicodeValue] = unicodeSchema!;

expect(genericValue.properties.next.$ref).toBe(`#/components/schemas/${genericName}`);
expect(unicodeValue.properties.next.$ref).toBe(`#/components/schemas/${unicodeName}`);

const bundledJson = JSON.stringify(schema);
expect(bundledJson).not.toContain('PaginatedList%3CClientItem%3E');
expect(bundledJson).not.toContain('%C3%9Cberschrift');
} finally {
fs.rmSync(tempDir, { force: true, recursive: true });
}
});

it('bundles multiple references to the same file correctly', async () => {
const refParser = new $RefParser();
const pathOrUrlOrSchema = path.join(
Expand Down
23 changes: 23 additions & 0 deletions packages/json-schema-ref-parser/src/__tests__/pointer.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,32 @@
import path from 'node:path';

import { $RefParser } from '..';
import Pointer from '../pointer';
import { getSpecsPath } from './utils';

describe('pointer', () => {
it('round-trips generic and unicode component names through join and parse', () => {
const genericRef = Pointer.join('#/components/schemas', 'PaginatedListItems<ClientResponse>');
const unicodeRef = Pointer.join('#/components/schemas', 'Überschrift');

expect(genericRef).toBe('#/components/schemas/PaginatedListItems%3CClientResponse%3E');
expect(unicodeRef).toBe('#/components/schemas/%C3%9Cberschrift');

expect(Pointer.parse(genericRef)).toEqual([
'components',
'schemas',
'PaginatedListItems<ClientResponse>',
]);
expect(Pointer.parse(unicodeRef)).toEqual(['components', 'schemas', 'Überschrift']);
});

it('preserves JSON Pointer escaping for path-like tokens while decoding them on parse', () => {
const joined = Pointer.join('#/paths', '/foo');

expect(joined).toBe('#/paths/~1foo');
expect(Pointer.parse(joined)).toEqual(['paths', '/foo']);
});

it('inlines internal JSON Pointer refs under #/paths/ for OpenAPI bundling', async () => {
const refParser = new $RefParser();
const pathOrUrlOrSchema = path.join(
Expand Down
4 changes: 2 additions & 2 deletions packages/json-schema-ref-parser/src/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,15 +574,15 @@ function remap(parser: $RefParser, inventory: Array<InventoryEntry>) {
// preserve the original $ref rather than rewriting it to the resolved hash.
if (!entry.external) {
if (!entry.extended && entry.$ref && typeof entry.$ref === 'object') {
entry.$ref.$ref = entry.hash;
entry.$ref.$ref = decodeURI(entry.hash);
}
continue;
}

// Avoid changing direct self-references; keep them internal
if (entry.circular) {
if (entry.$ref && typeof entry.$ref === 'object') {
entry.$ref.$ref = entry.pathFromRoot;
entry.$ref.$ref = decodeURI(entry.pathFromRoot);
}
continue;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/json-schema-ref-parser/src/dereference.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ function dereference$Ref<S extends object = JSONSchema>(
if (directCircular) {
// The pointer is a DIRECT circular reference (i.e., it references itself).
// So replace the $ref path with the absolute path from the JSON Schema root
dereferencedValue.$ref = pathFromRoot;
dereferencedValue.$ref = decodeURI(pathFromRoot);
}

const dereferencedObject = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1043,7 +1043,7 @@ export type AdditionalPropertiesUnknownIssueWritable = {
[key: string]: string | number;
};

export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBoolean) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemString;
export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBooleanWritable) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemStringWritable;

export type GenericSchemaDuplicateIssue1SystemBooleanWritable = {
item?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1043,7 +1043,7 @@ export type AdditionalPropertiesUnknownIssueWritable = {
[key: string]: string | number;
};

export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBoolean) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemString;
export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBooleanWritable) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemStringWritable;

export type GenericSchemaDuplicateIssue1SystemBooleanWritable = {
item?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2356,7 +2356,7 @@ export const OneOfAllOfIssueWritableSchema = {
$ref: '#/components/schemas/ConstValue'
},
{
$ref: '#/components/schemas/Generic.Schema.Duplicate.Issue`1[System.Boolean]'
$ref: '#/components/schemas/Generic.Schema.Duplicate.Issue`1[System.Boolean]Writable'
}
]
},
Expand All @@ -2366,7 +2366,7 @@ export const OneOfAllOfIssueWritableSchema = {
]
},
{
$ref: '#/components/schemas/Generic.Schema.Duplicate.Issue`1[System.String]'
$ref: '#/components/schemas/Generic.Schema.Duplicate.Issue`1[System.String]Writable'
}
]
} as const;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1043,7 +1043,7 @@ export type AdditionalPropertiesUnknownIssueWritable = {
[key: string]: string | number;
};

export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBoolean) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemString;
export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBooleanWritable) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemStringWritable;

export type GenericSchemaDuplicateIssue1SystemBooleanWritable = {
item?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1043,7 +1043,7 @@ export type AdditionalPropertiesUnknownIssueWritable = {
[key: string]: string | number;
};

export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBoolean) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemString;
export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBooleanWritable) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemStringWritable;

export type GenericSchemaDuplicateIssue1SystemBooleanWritable = {
item?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1043,7 +1043,7 @@ export type AdditionalPropertiesUnknownIssueWritable = {
[key: string]: string | number;
};

export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBoolean) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemString;
export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBooleanWritable) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemStringWritable;

export type GenericSchemaDuplicateIssue1SystemBooleanWritable = {
item?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1043,7 +1043,7 @@ export type AdditionalPropertiesUnknownIssueWritable = {
[key: string]: string | number;
};

export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBoolean) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemString;
export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBooleanWritable) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemStringWritable;

export type GenericSchemaDuplicateIssue1SystemBooleanWritable = {
item?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1043,7 +1043,7 @@ export type AdditionalPropertiesUnknownIssueWritable = {
[key: string]: string | number;
};

export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBoolean) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemString;
export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBooleanWritable) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemStringWritable;

export type GenericSchemaDuplicateIssue1SystemBooleanWritable = {
item?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1043,7 +1043,7 @@ export type AdditionalPropertiesUnknownIssueWritable = {
[key: string]: string | number;
};

export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBoolean) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemString;
export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBooleanWritable) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemStringWritable;

export type GenericSchemaDuplicateIssue1SystemBooleanWritable = {
item?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1043,7 +1043,7 @@ export type AdditionalPropertiesUnknownIssueWritable = {
[key: string]: string | number;
};

export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBoolean) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemString;
export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBooleanWritable) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemStringWritable;

export type GenericSchemaDuplicateIssue1SystemBooleanWritable = {
item?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1043,7 +1043,7 @@ export type AdditionalPropertiesUnknownIssueWritable = {
[key: string]: string | number;
};

export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBoolean) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemString;
export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBooleanWritable) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemStringWritable;

export type GenericSchemaDuplicateIssue1SystemBooleanWritable = {
item?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1043,7 +1043,7 @@ export type AdditionalPropertiesUnknownIssueWritable = {
[key: string]: string | number;
};

export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBoolean) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemString;
export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBooleanWritable) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemStringWritable;

export type GenericSchemaDuplicateIssue1SystemBooleanWritable = {
item?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1043,7 +1043,7 @@ export type AdditionalPropertiesUnknownIssueWritable = {
[key: string]: string | number;
};

export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBoolean) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemString;
export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBooleanWritable) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemStringWritable;

export type GenericSchemaDuplicateIssue1SystemBooleanWritable = {
item?: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1043,7 +1043,7 @@ export type AdditionalPropertiesUnknownIssueWritable = {
[key: string]: string | number;
};

export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBoolean) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemString;
export type OneOfAllOfIssueWritable = ((ConstValue | GenericSchemaDuplicateIssue1SystemBooleanWritable) & _3eNum1Период) | GenericSchemaDuplicateIssue1SystemStringWritable;

export type GenericSchemaDuplicateIssue1SystemBooleanWritable = {
item?: boolean;
Expand Down
Loading
Loading