JSON Schema数据转换技术指南
时间:2025-5-21 14:18 作者:六度科技 分类: JS
JSON Schema数据转换技术指南
1. JSON Schema转换为结构化文本
1.1 转换原理
JSON Schema转换为结构化文本的核心思路是:
- 利用Schema中的
description
字段作为字段标签 - 通过递归遍历处理嵌套结构
- 使用缩进和特殊符号表示层次关系
1.2 实现代码
/**
* 将JSON Schema数据转换为结构化文本
* @param {Object} data - 包含Schema和数据的对象
* @returns {string} 格式化的结构化文本
*/
function jsonSchemaToText(data) {
let output = '';
// 添加标题
if (data.title) {
output += `[${data.title}]\n`;
}
// 递归解析函数
function parseNode(node, schemaNode, depth = 0) {
let result = '';
const indent = ' '.repeat(depth);
const indentItem = ' '.repeat(depth + 1);
// 处理数组
if (Array.isArray(node)) {
node.forEach((item, index) => {
if (typeof item === 'object' && item !== null) {
result += `${indent}• ${index + 1}:\n`;
result += parseNode(item, schemaNode.items, depth + 1);
} else {
result += `${indent}• ${item}\n`;
}
});
}
// 处理对象
else if (typeof node === 'object' && node !== null) {
for (const key in node) {
const value = node[key];
const propSchema = schemaNode.properties?.[key];
const label = propSchema?.description || key;
if (typeof value === 'object' && value !== null) {
if (Array.isArray(value)) {
result += `${indent}[${label}]\n`;
value.forEach(item => {
if (typeof item === 'object') {
result += parseNode(item, propSchema.items, depth + 1);
} else {
result += `${indentItem}• ${item}\n`;
}
});
} else {
result += `${indent}[${label}]\n`;
result += parseNode(value, propSchema, depth + 1);
}
} else {
result += `${indent}• ${label}: ${value}\n`;
}
}
}
// 处理基本类型
else {
result += `${indent}${node}\n`;
}
return result;
}
// 过滤schema元数据字段
const mainData = {};
for (const key in data) {
if (!key.startsWith('$') && !['title', 'type', 'required', 'properties'].includes(key)) {
mainData[key] = data[key];
}
}
output += parseNode(mainData, data);
return output.trim();
}
1.3 使用示例
// 使用示例
const jsonData = {
"title": "场景数据",
"properties": {
"name": {
"type": "string",
"description": "场景名称"
},
"description": {
"type": "array",
"description": "场景描述",
"items": { "type": "string" }
}
},
"name": "白鹿部落营地",
"description": [
"由数十个兽皮帐篷组成",
"营地中央有篝火"
]
};
const text = jsonSchemaToText(jsonData);
console.log(text);
/* 输出:
[场景数据]
• 场景名称: 白鹿部落营地
[场景描述]
• 由数十个兽皮帐篷组成
• 营地中央有篝火
*/
2. JSON Schema转换为表格
2.1 转换原理
将JSON Schema转换为表格的核心思路是:
- 根据数据类型生成不同的表格结构
- 为每个字段添加数据路径属性,跟踪其在JSON结构中的位置
- 支持嵌套对象和数组的表格表示
- 处理编辑模式和只读模式的差异
2.2 实现代码
/**
* 将JSON Schema数据转换为HTML表格
* @param {Object} data - 包含Schema和数据的对象
* @param {boolean} isEditMode - 是否为可编辑模式
* @returns {string} HTML表格代码
*/
function jsonSchemaToTable(data, isEditMode = false) {
let html = '<div class="json-table">';
// 提取schema信息
const schema = {
properties: {},
...data
};
// 添加标题
if (data.title) {
html += `<div class="json-table-title">${data.title}</div>`;
}
// 递归生成表格
function generateTableContent(obj, path = '', schemaInfo = null) {
let content = '';
// 处理对象
if (typeof obj === 'object' && obj !== null && !Array.isArray(obj)) {
content += '<table class="json-object-table">';
// 获取schema信息
const properties = (schemaInfo && schemaInfo.properties) || (schema.properties) || {};
// 遍历对象属性
for (const key in obj) {
// 跳过schema元数据
if (['$schema', 'properties', 'required', 'type', 'items'].includes(key)) {
continue;
}
const value = obj[key];
const propSchema = properties[key] || {};
const label = propSchema.description || key;
const fieldPath = path ? `${path}.${key}` : key;
if (typeof value === 'object' && value !== null) {
if (Array.isArray(value)) {
// 数组类型
content += `<tr><th colspan="2" class="json-section-header" data-path="${fieldPath}">${label}</th></tr>`;
content += `<tr><td colspan="2" class="json-array-container">`;
if (value.length > 0) {
if (typeof value[0] === 'object' && value[0] !== null) {
// 对象数组 - 表格形式
content += renderObjectArray(value, fieldPath, propSchema, isEditMode);
} else {
// 简单数组 - 列表形式
content += renderSimpleArray(value, fieldPath, isEditMode);
}
} else {
// 空数组
content += `<div class="json-empty-array" data-array-path="${fieldPath}">
<span>空数组</span>
${isEditMode ? `<button class="add-array-item" data-path="${fieldPath}">添加项</button>` : ''}
</div>`;
}
content += '</td></tr>';
} else {
// 对象类型
content += `<tr><th colspan="2" class="json-section-header" data-path="${fieldPath}">${label}</th></tr>`;
content += `<tr><td colspan="2">${generateTableContent(value, fieldPath, propSchema)}</td></tr>`;
}
} else {
// 基本类型值
const editable = isEditMode && !isHeaderField(key);
content += `<tr data-field-path="${fieldPath}">
<th>${label}</th>
<td class="json-value ${editable ? 'editable' : ''}"
data-path="${fieldPath}"
data-type="${typeof value}">${value}</td>
</tr>`;
}
}
content += '</table>';
}
// 处理数组
else if (Array.isArray(obj)) {
// 数组处理逻辑...
content += renderArrayContent(obj, path, schemaInfo, isEditMode);
}
// 基本类型
else {
content += `<span class="json-value" data-type="${typeof obj}">${obj}</span>`;
}
return content;
}
// 处理对象数组渲染
function renderObjectArray(array, path, schema, isEditMode) {
// 实现对象数组的表格渲染...
}
// 处理简单数组渲染
function renderSimpleArray(array, path, isEditMode) {
// 实现简单数组的列表渲染...
}
// 判断字段是否为标题/头部字段
function isHeaderField(key) {
return ['title', 'name', 'id', 'type', 'category'].includes(key);
}
// 过滤schema元数据
const mainData = {};
for (const key in data) {
if (key !== '$schema') {
mainData[key] = data[key];
}
}
html += generateTableContent(mainData);
html += '</div>';
return html;
}
2.3 使用示例
// 使用示例
const jsonData = {
"title": "角色数据",
"properties": {
"name": {
"type": "string",
"description": "角色名称"
},
"skills": {
"type": "array",
"description": "技能列表",
"items": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "技能名称"},
"level": {"type": "number", "description": "技能等级"}
}
}
}
},
"name": "林毅",
"skills": [
{"name": "探索", "level": 5},
{"name": "生存", "level": 3}
]
};
// 生成只读表格
const readOnlyTable = jsonSchemaToTable(jsonData, false);
// 生成可编辑表格
const editableTable = jsonSchemaToTable(jsonData, true);
3. 表格转换回JSON Schema
3.1 转换原理
将表格数据转回JSON Schema的核心思路是:
- 从表格元素中提取数据路径和值
- 使用路径更新JSON对象
- 保留原始Schema的结构和元数据
- 处理各种类型的数据(基本类型、数组、对象)
3.2 实现代码
/**
* 将表格中的编辑内容更新到JSON对象
* @param {HTMLElement} tableElement - 表格DOM元素
* @param {Object} originalSchema - 原始JSON Schema对象
* @returns {Object} 更新后的JSON对象
*/
function tableToJsonSchema(tableElement, originalSchema) {
// 创建结果对象,保留原始schema元数据
const result = { ...originalSchema };
// 处理所有可编辑单元格
const editableCells = tableElement.querySelectorAll('.json-value');
editableCells.forEach(cell => {
const path = cell.dataset.path;
if (!path) return;
const type = cell.dataset.type || 'string';
let value = cell.textContent.trim();
// 根据类型转换值
if (type === 'number') {
value = Number(value);
} else if (type === 'boolean') {
value = value.toLowerCase() === 'true';
}
// 更新JSON对象中的指定路径
updateJsonByPath(result, path, value);
});
// 处理数组项
const arrayItems = tableElement.querySelectorAll('.json-array-container');
arrayItems.forEach(container => {
const arrayPath = container.dataset.arrayPath;
if (!arrayPath) return;
// 处理对象数组
const arrayTable = container.querySelector('.json-array-table');
if (arrayTable) {
const rows = Array.from(arrayTable.querySelectorAll('tr')).filter(
row => row.dataset.arrayPath === arrayPath
);
// 收集数组项的数据
const arrayItems = [];
rows.forEach(row => {
const index = parseInt(row.dataset.arrayIndex);
if (isNaN(index)) return;
// 创建数组项对象
const item = {};
const cells = row.querySelectorAll('.json-value');
cells.forEach(cell => {
const path = cell.dataset.path;
if (!path) return;
// 从路径中提取键名
const keyMatch = path.match(/\[[\d]+\]\.(\w+)$/);
if (keyMatch && keyMatch[1]) {
const key = keyMatch[1];
const type = cell.dataset.type || 'string';
let value = cell.textContent.trim();
// 根据类型转换值
if (type === 'number') {
value = Number(value);
} else if (type === 'boolean') {
value = value.toLowerCase() === 'true';
}
item[key] = value;
}
});
arrayItems[index] = item;
});
// 更新JSON对象
updateJsonByPath(result, arrayPath, arrayItems);
}
// 处理简单数组
const simpleArray = container.querySelector('.json-simple-array');
if (simpleArray) {
const items = [];
const itemElements = simpleArray.querySelectorAll('.json-array-item');
itemElements.forEach(itemElement => {
const valueElement = itemElement.querySelector('.json-value');
if (!valueElement) return;
const type = valueElement.dataset.type || 'string';
let value = valueElement.textContent.trim();
// 根据类型转换值
if (type === 'number') {
value = Number(value);
} else if (type === 'boolean') {
value = value.toLowerCase() === 'true';
}
items.push(value);
});
// 更新JSON对象
updateJsonByPath(result, arrayPath, items);
}
});
return result;
}
/**
* 更新JSON对象中指定路径的值
* @param {Object} jsonObj - JSON对象
* @param {string} path - 属性路径(例如:'basicInfo.location')
* @param {any} value - 新值
*/
function updateJsonByPath(jsonObj, path, value) {
const parts = path.split('.');
let current = jsonObj;
// 遍历路径直到最后一项之前
for (let i = 0; i < parts.length - 1; i++) {
let part = parts[i];
// 处理数组索引,例如:items[0]
const arrayMatch = part.match(/(\w+)\[(\d+)\]/);
if (arrayMatch) {
const arrayName = arrayMatch[1];
const arrayIndex = parseInt(arrayMatch[2]);
if (!current[arrayName]) {
current[arrayName] = [];
}
if (!current[arrayName][arrayIndex]) {
current[arrayName][arrayIndex] = {};
}
current = current[arrayName][arrayIndex];
} else {
if (!current[part]) {
current[part] = {};
}
current = current[part];
}
}
// 设置最终属性的值
const lastPart = parts[parts.length - 1];
// 处理数组索引
const arrayMatch = lastPart.match(/(\w+)\[(\d+)\]/);
if (arrayMatch) {
const arrayName = arrayMatch[1];
const arrayIndex = parseInt(arrayMatch[2]);
if (!current[arrayName]) {
current[arrayName] = [];
}
current[arrayName][arrayIndex] = value;
} else {
current[lastPart] = value;
}
}
3.3 使用示例
// 使用示例
// 1. 假设我们有一个表格元素和原始Schema
const tableElement = document.querySelector('.json-table');
const originalSchema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "角色数据",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "角色名称"
},
"skills": {
"type": "array",
"description": "技能列表",
"items": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "技能名称"},
"level": {"type": "number", "description": "技能等级"}
}
}
}
}
};
// 2. 从表格中提取更新后的数据
const updatedSchema = tableToJsonSchema(tableElement, originalSchema);
// 3. 转换为JSON字符串以便存储或发送到服务器
const jsonString = JSON.stringify(updatedSchema, null, 2);
4. 实际应用示例
4.1 完整工作流程
下面是一个完整的工作流程示例,展示了JSON Schema在编辑界面中的应用:
// 1. 从服务器获取JSON Schema数据
async function loadSchemaData(path) {
const response = await fetch(`/api/file?path=${encodeURIComponent(path)}`);
const data = await response.json();
return data.content;
}
// 2. 初始化编辑器
async function initEditor(path) {
// 加载数据
const schemaJson = await loadSchemaData(path);
const schemaObj = JSON.parse(schemaJson);
// 获取DOM元素
const previewArea = document.getElementById('preview-area');
const editArea = document.getElementById('edit-area');
const editorTextarea = document.getElementById('editor-textarea');
// 变量跟踪状态
let isEditMode = false;
let isTableMode = true;
// 切换编辑/预览模式
function toggleEditMode() {
isEditMode = !isEditMode;
if (isEditMode) {
// 切换到编辑模式
if (isTableMode) {
// 表格编辑模式
const tableHtml = jsonSchemaToTable(schemaObj, true);
previewArea.innerHTML = tableHtml;
setupTableEditing(schemaObj, previewArea);
editArea.style.display = 'none';
previewArea.style.display = 'block';
} else {
// 文本编辑模式
editorTextarea.value = JSON.stringify(schemaObj, null, 2);
editArea.style.display = 'block';
previewArea.style.display = 'none';
}
} else {
// 切换到预览模式
if (isTableMode) {
// 表格预览
const tableHtml = jsonSchemaToTable(schemaObj, false);
previewArea.innerHTML = tableHtml;
} else {
// 文本预览
const structuredText = jsonSchemaToText(schemaObj);
previewArea.innerHTML = `<pre>${structuredText}</pre>`;
}
editArea.style.display = 'none';
previewArea.style.display = 'block';
}
}
// 切换表格/文本模式
function toggleTableMode() {
isTableMode = !isTableMode;
if (isEditMode) {
if (isTableMode) {
// 从文本编辑切换到表格编辑
try {
schemaObj = JSON.parse(editorTextarea.value);
const tableHtml = jsonSchemaToTable(schemaObj, true);
previewArea.innerHTML = tableHtml;
setupTableEditing(schemaObj, previewArea);
editArea.style.display = 'none';
previewArea.style.display = 'block';
} catch (e) {
alert('JSON格式错误,无法切换到表格模式');
isTableMode = false;
}
} else {
// 从表格编辑切换到文本编辑
const updatedSchema = tableToJsonSchema(
previewArea.querySelector('.json-table'),
schemaObj
);
schemaObj = updatedSchema;
editorTextarea.value = JSON.stringify(schemaObj, null, 2);
editArea.style.display = 'block';
previewArea.style.display = 'none';
}
} else {
// 预览模式下切换
if (isTableMode) {
const tableHtml = jsonSchemaToTable(schemaObj, false);
previewArea.innerHTML = tableHtml;
} else {
const structuredText = jsonSchemaToText(schemaObj);
previewArea.innerHTML = `<pre>${structuredText}</pre>`;
}
}
}
// 保存数据
async function saveData() {
let dataToSave;
if (isEditMode) {
if (isTableMode) {
// 从表格获取更新的数据
const updatedSchema = tableToJsonSchema(
previewArea.querySelector('.json-table'),
schemaObj
);
dataToSave = JSON.stringify(updatedSchema, null, 2);
} else {
// 从文本编辑器获取数据
dataToSave = editorTextarea.value;
}
} else {
dataToSave = JSON.stringify(schemaObj, null, 2);
}
// 发送到服务器
await fetch(`/api/file?path=${encodeURIComponent(path)}`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: dataToSave
});
alert('保存成功');
}
// 初始显示
const tableHtml = jsonSchemaToTable(schemaObj, false);
previewArea.innerHTML = tableHtml;
// 绑定按钮事件
document.getElementById('edit-mode-btn').addEventListener('click', toggleEditMode);
document.getElementById('table-mode-btn').addEventListener('click', toggleTableMode);
document.getElementById('save-btn').addEventListener('click', saveData);
}
// 调用初始化
initEditor('数据/场景/原始森林.json');
4.2 表格编辑事件处理
/**
* 设置表格编辑功能
*/
function setupTableEditing(jsonObj, tableContainer) {
// 事件委托处理表格交互
tableContainer.addEventListener('click', function(event) {
const target = event.target;
// 处理可编辑单元格点击
if (target.classList.contains('json-value') && target.classList.contains('editable')) {
handleEditableCellClick(target, event, jsonObj);
}
// 处理添加数组项按钮
else if (target.classList.contains('add-array-item')) {
const arrayPath = target.dataset.path;
addArrayItem(jsonObj, arrayPath, tableContainer);
}
// 处理删除数组项按钮
else if (target.classList.contains('delete-array-item')) {
const arrayPath = target.dataset.path;
const index = parseInt(target.dataset.index);
deleteArrayItem(jsonObj, arrayPath, index, tableContainer);
}
});
}
5. 技术注意事项
5.1 路径表示法
在这个实现中,我们使用点分隔的路径表示法来表示JSON数据中的位置:
- 普通属性:
basicInfo.location
- 数组索引:
description[0]
- 嵌套数组对象属性:
characters[2].skills[1].name
这种路径表示法允许我们在复杂嵌套结构中精确定位和更新值。
5.2 性能考虑
对于大型JSON数据,可以考虑以下性能优化:
- 懒加载:只渲染当前可见的数据部分
- 虚拟滚动:处理大型数组时使用虚拟滚动技术
- 内存管理:避免创建过多临时对象
5.3 安全考虑
在实际应用中需要注意:
- 输入验证:确保用户输入符合Schema定义
- XSS防护:在显示用户输入前进行适当的HTML转义
- 类型检查:确保类型转换正确,防止数据损坏
6. 总结
JSON Schema提供了一种强大的方式来定义、存储和验证结构化数据。通过本文介绍的三种转换技术:
- JSON Schema到结构化文本
- JSON Schema到编辑表格
- 表格编辑内容回到JSON Schema
开发者可以构建用户友好的编辑界面,同时保持数据的结构化特性。这种方法特别适合需要复杂结构化数据管理的应用,如内容管理系统、游戏数据编辑器或小说创作辅助工具。
通过这些技术,系统既提供了严格的数据结构定义,又能够以直观的方式呈现给用户进行查看和编辑。