Skip to content
Open
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
2 changes: 1 addition & 1 deletion packages/elements-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stoplight/elements-core",
"version": "9.0.19",
"version": "9.0.20",
"sideEffects": [
"web-components.min.js",
"src/web-components/**",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { isPlainObject, safeStringify } from '@stoplight/json';
import * as Sampler from '@stoplight/json-schema-sampler';
import { IMediaTypeContent, INodeExample, INodeExternalExample } from '@stoplight/types';
import { JSONSchema7 } from 'json-schema';
import { JSONSchema7, JSONSchema7Object, JSONSchema7Type } from 'json-schema';
import React from 'react';

import { useDocument } from '../../context/InlineRefResolver';
Expand Down Expand Up @@ -60,16 +60,49 @@ export const generateExampleFromMediaTypeContent = (
return '';
};

export const generateExamplesFromJsonSchema = (schema: JSONSchema7 & { 'x-examples'?: unknown }): Example[] => {
export const generateExamplesFromJsonSchema = (schema: JSONSchema7 & { 'x-examples'?: JSONSchema7Type }): Example[] => {
const examples: Example[] = [];
const hasResolvedProperties = (schemaToCheck: JSONSchema7): boolean => {
// Case 1: Direct object with properties
if (schemaToCheck.properties && Object.keys(schemaToCheck.properties).length > 0) {
return true;
}

// Case 2: Check inside allOf, oneOf, anyOf recursively
const composedArray = schemaToCheck.allOf || schemaToCheck.oneOf || schemaToCheck.anyOf;
if (Array.isArray(composedArray)) {
return composedArray.some(sub => {
if (typeof sub !== 'object' || sub === null) return false;
return hasResolvedProperties(sub as JSONSchema7);
});
}

return false;
};

const hasNoResolvedProperties = (schemaToCheck: JSONSchema7): boolean => {
return !hasResolvedProperties(schemaToCheck);
};

const isHasNoResolvedProperties = hasNoResolvedProperties(schema);

if (Array.isArray(schema?.examples)) {
schema.examples.forEach((example, index) => {
examples.push({
data: safeStringify(example, undefined, 2) ?? '',
label: index === 0 ? 'default' : `example-${index}`,
if (isHasNoResolvedProperties) {
schema.examples.forEach((example, index) => {
examples.push({
data: '{}',
label: index === 0 ? 'default' : `example-${index}`,
});
});
});
} else {
let res = filterExamplesBySchema(schema, schema.examples);
res.forEach((example, index) => {
examples.push({
data: safeStringify(example, undefined, 2) ?? '',
label: index === 0 ? 'default' : `example-${index}`,
});
});
}
} else if (isPlainObject(schema?.['x-examples'])) {
for (const [label, example] of Object.entries(schema['x-examples'])) {
if (isPlainObject(example)) {
Expand Down Expand Up @@ -108,3 +141,113 @@ export const generateExamplesFromJsonSchema = (schema: JSONSchema7 & { 'x-exampl
export const exceedsSize = (example: string, size: number = 500) => {
return example.split(/\r\n|\r|\n/).length > size;
};

/**
* Filters examples to only include properties that exist in the schema.
* Handles nested objects, arrays, allOf, oneOf, anyOf, and additionalProperties.
* Only removes a property from the example at the exact path where it was removed from the schema.
*
* @param schema - The JSON Schema (possibly with masked/hidden properties)
* @param examples - Array of raw JSON values (e.g. schema.examples)
* @returns New array of filtered objects matching the schema structure
*/
export const filterExamplesBySchema = (
schema: JSONSchema7 & { 'x-examples'?: JSONSchema7Type },
examples: JSONSchema7Type[],
): JSONSchema7Type[] => {
return examples.map(example => {
try {
return filterValueBySchema(example, schema);
} catch {
return example;
}
});
};

const collectSchemaPropertyNames = (schema: JSONSchema7): Set<string> => {
const keys = new Set<string>();

if (schema.properties) {
for (const key of Object.keys(schema.properties)) {
keys.add(key);
}
}

const composedSchemas = [
...(Array.isArray(schema.allOf) ? schema.allOf : []),
...(Array.isArray(schema.oneOf) ? schema.oneOf : []),
...(Array.isArray(schema.anyOf) ? schema.anyOf : []),
];

for (const sub of composedSchemas) {
if (typeof sub === 'object' && sub !== null) {
for (const key of collectSchemaPropertyNames(sub as JSONSchema7)) {
keys.add(key);
}
}
}

return keys;
};

const findPropertySchema = (schema: JSONSchema7, propertyName: string): JSONSchema7 | undefined => {
if (schema.properties?.[propertyName]) {
const prop = schema.properties[propertyName];
return typeof prop === 'boolean' ? undefined : prop;
}

const composedSchemas = [
...(Array.isArray(schema.allOf) ? schema.allOf : []),
...(Array.isArray(schema.oneOf) ? schema.oneOf : []),
...(Array.isArray(schema.anyOf) ? schema.anyOf : []),
];

for (const sub of composedSchemas) {
if (typeof sub === 'object' && sub !== null) {
const found = findPropertySchema(sub as JSONSchema7, propertyName);
if (found) return found;
}
}

return undefined;
};

const filterValueBySchema = (value: JSONSchema7Type, schema: JSONSchema7): JSONSchema7Type => {
if (value === null || value === undefined) return value;

// Handle arrays
if (Array.isArray(value)) {
const itemSchema =
schema.items && typeof schema.items !== 'boolean' && !Array.isArray(schema.items)
? (schema.items as JSONSchema7)
: undefined;

return itemSchema ? value.map(item => filterValueBySchema(item, itemSchema)) : value;
}

// Handle objects
if (isPlainObject(value)) {
const allowedKeys = collectSchemaPropertyNames(schema);
const hasStructure = allowedKeys.size > 0;
const hasAdditionalProperties = schema.additionalProperties;

if (!hasStructure && !hasAdditionalProperties) return value as JSONSchema7Object;

const result: JSONSchema7Object = {};

for (const [key, val] of Object.entries(value as JSONSchema7Object)) {
if (allowedKeys.has(key)) {
const propSchema = findPropertySchema(schema, key);
result[key] = propSchema ? filterValueBySchema(val as JSONSchema7Type, propSchema) : val;
} else if (hasAdditionalProperties) {
result[key] = val;
}
// else: property was masked/removed from schema — omit it
}

return result;
}

// Primitives
return value;
};
4 changes: 2 additions & 2 deletions packages/elements-dev-portal/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stoplight/elements-dev-portal",
"version": "3.0.19",
"version": "3.0.20",
"description": "UI components for composing beautiful developer documentation.",
"keywords": [],
"sideEffects": [
Expand Down Expand Up @@ -66,7 +66,7 @@
"dependencies": {
"@stoplight/markdown-viewer": "^5.7.1",
"@stoplight/mosaic": "^1.53.5",
"@stoplight/elements-core": "~9.0.19",
"@stoplight/elements-core": "~9.0.20",
"@stoplight/path": "^1.3.2",
"@stoplight/types": "^14.0.0",
"classnames": "^2.2.6",
Expand Down
4 changes: 2 additions & 2 deletions packages/elements/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stoplight/elements",
"version": "9.0.19",
"version": "9.0.20",
"description": "UI components for composing beautiful developer documentation.",
"keywords": [],
"sideEffects": [
Expand Down Expand Up @@ -63,7 +63,7 @@
]
},
"dependencies": {
"@stoplight/elements-core": "~9.0.19",
"@stoplight/elements-core": "~9.0.20",
"@stoplight/http-spec": "^7.1.0",
"@stoplight/json": "^3.18.1",
"@stoplight/mosaic": "^1.53.5",
Expand Down