apx-toolkit
Version:
Automatically discover APIs and generate complete integration packages: code in 12 languages, TypeScript types, test suites, SDK packages, API documentation, mock servers, performance reports, and contract tests. Saves 2-4 weeks of work in seconds.
222 lines • 7.29 kB
JavaScript
/**
* JSON Schema Generator
* Generates JSON Schema from API responses for validation and OpenAPI integration
* Based on JSON Schema best practices and OpenAPI 3.1 specifications
*/
/**
* Generates JSON Schema from API response data
* Follows JSON Schema best practices:
* - Uses $ref for reusability
* - Avoids overusing allOf/anyOf/oneOf
* - Includes proper types and formats
* - Adds descriptions and examples
*/
export function generateJSONSchema(data, schemaName, depth = 0, maxDepth = 10) {
// Prevent infinite recursion
if (depth > maxDepth) {
return {
type: 'object',
description: 'Complex nested structure',
additionalProperties: true,
};
}
// Handle null/undefined
// OpenAPI 3.1 best practice: use nullable: true instead of type: 'null'
if (data === null || data === undefined) {
return {
type: 'string', // Default to string, can be overridden by context
nullable: true,
description: 'Nullable value',
};
}
// Handle arrays
if (Array.isArray(data)) {
if (data.length === 0) {
return {
type: 'array',
items: {
type: 'object',
additionalProperties: true,
},
description: 'Empty array',
};
}
// Generate schema for first item (assume homogeneous arrays)
const itemSchema = generateJSONSchema(data[0], `${schemaName}Item`, depth + 1, maxDepth);
return {
type: 'array',
items: itemSchema,
description: `Array of ${itemSchema.type || 'items'}`,
example: data.slice(0, 3), // Include up to 3 examples
};
}
// Handle primitives
if (typeof data !== 'object') {
const schema = {
type: getJSONSchemaType(typeof data),
};
// Add format for specific types
if (typeof data === 'string') {
const format = inferStringFormat(data);
if (format) {
schema.format = format;
}
schema.minLength = data.length > 0 ? 1 : 0;
schema.maxLength = data.length;
schema.example = data;
}
else if (typeof data === 'number') {
schema.example = data;
if (Number.isInteger(data)) {
schema.type = 'integer';
}
}
else if (typeof data === 'boolean') {
schema.example = data;
}
return schema;
}
// Handle objects
const obj = data;
const properties = {};
const required = [];
for (const [key, value] of Object.entries(obj)) {
// Skip metadata fields
if (key.startsWith('_')) {
continue;
}
const propSchema = generateJSONSchema(value, `${schemaName}_${key}`, depth + 1, maxDepth);
// Add description based on field name
propSchema.description = propSchema.description || inferFieldDescription(key, value);
// Add example
if (value !== null && value !== undefined) {
propSchema.example = value;
}
// OpenAPI 3.1: Mark as nullable if value is null (but still allow the type)
if (value === null) {
propSchema.nullable = true;
// Keep the type from context or default to string
if (!propSchema.type) {
propSchema.type = 'string';
}
}
properties[key] = propSchema;
// Mark as required if not null/undefined
if (value !== null && value !== undefined) {
required.push(key);
}
}
return {
type: 'object',
properties,
required: required.length > 0 ? required : undefined,
additionalProperties: false, // Strict schema - no extra properties
description: schemaName ? `Schema for ${schemaName}` : 'Object schema',
example: obj,
};
}
/**
* Infers JSON Schema type from JavaScript type
*/
function getJSONSchemaType(jsType) {
const typeMap = {
string: 'string',
number: 'number',
boolean: 'boolean',
object: 'object',
array: 'array',
};
return typeMap[jsType] || 'string';
}
/**
* Infers string format from value
*/
function inferStringFormat(value) {
// Email
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
return 'email';
}
// URI/URL
if (/^https?:\/\//.test(value)) {
return 'uri';
}
// Date (ISO 8601)
if (/^\d{4}-\d{2}-\d{2}/.test(value) && !isNaN(Date.parse(value))) {
return 'date-time';
}
// UUID
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value)) {
return 'uuid';
}
// IP address
if (/^(\d{1,3}\.){3}\d{1,3}$/.test(value)) {
return 'ipv4';
}
return undefined;
}
/**
* Infers human-readable description for fields
*/
function inferFieldDescription(fieldName, value) {
const name = fieldName.toLowerCase();
const patterns = [
[/^id$|_id$/, 'Unique identifier'],
[/^name$|_name$/, 'Name of the item'],
[/^email$|_email$/, 'Email address'],
[/^url$|_url$/, 'URL or web address'],
[/^date$|_date$|Date$/, 'Date value'],
[/^time$|_time$|timestamp$|_at$/, 'Timestamp'],
[/^page$|_page$/, 'Page number'],
[/^limit$|_limit$/, 'Maximum number of items'],
[/^offset$|_offset$/, 'Number of items to skip'],
[/^total$|_total$/, 'Total count'],
[/^count$|_count$/, 'Count of items'],
[/^status$|_status$/, 'Status value'],
[/^type$|_type$/, 'Type or category'],
[/^title$|_title$/, 'Title'],
[/^description$|_description$/, 'Description'],
[/^created$|created_at$/, 'Creation timestamp'],
[/^updated$|updated_at$/, 'Last update timestamp'],
[/^user$|_user$/, 'User information'],
[/^token$|_token$/, 'Authentication token'],
[/^key$|_key$/, 'API key or identifier'],
];
for (const [pattern, description] of patterns) {
if (pattern.test(name)) {
return description;
}
}
// Default based on type
if (typeof value === 'number') {
return `Numeric value: ${fieldName}`;
}
else if (typeof value === 'string') {
return `String value: ${fieldName}`;
}
else if (typeof value === 'boolean') {
return `Boolean flag: ${fieldName}`;
}
return `Field: ${fieldName}`;
}
/**
* Generates reusable schema components for OpenAPI
* Uses $ref for reusability (JSON Schema best practice)
*/
export function generateSchemaComponents(schemas) {
const components = {};
for (const [name, schema] of schemas.entries()) {
// Remove $schema if present (not needed in OpenAPI components)
const { $schema, ...openAPISchema } = schema;
components[name] = openAPISchema;
}
return components;
}
/**
* Creates a $ref reference to a component schema
*/
export function createSchemaRef(componentName) {
return {
$ref: `#/components/schemas/${componentName}`,
};
}
//# sourceMappingURL=json-schema-generator.js.map