«

JSON Schema数据转换技术指南

时间:2025-5-21 14:18     作者:六度科技     分类: JS


JSON Schema数据转换技术指南

1. JSON Schema转换为结构化文本

1.1 转换原理

JSON Schema转换为结构化文本的核心思路是:

  1. 利用Schema中的description字段作为字段标签
  2. 通过递归遍历处理嵌套结构
  3. 使用缩进和特殊符号表示层次关系

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转换为表格的核心思路是:

  1. 根据数据类型生成不同的表格结构
  2. 为每个字段添加数据路径属性,跟踪其在JSON结构中的位置
  3. 支持嵌套对象和数组的表格表示
  4. 处理编辑模式和只读模式的差异

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的核心思路是:

  1. 从表格元素中提取数据路径和值
  2. 使用路径更新JSON对象
  3. 保留原始Schema的结构和元数据
  4. 处理各种类型的数据(基本类型、数组、对象)

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数据中的位置:

这种路径表示法允许我们在复杂嵌套结构中精确定位和更新值。

5.2 性能考虑

对于大型JSON数据,可以考虑以下性能优化:

  1. 懒加载:只渲染当前可见的数据部分
  2. 虚拟滚动:处理大型数组时使用虚拟滚动技术
  3. 内存管理:避免创建过多临时对象

5.3 安全考虑

在实际应用中需要注意:

  1. 输入验证:确保用户输入符合Schema定义
  2. XSS防护:在显示用户输入前进行适当的HTML转义
  3. 类型检查:确保类型转换正确,防止数据损坏

6. 总结

JSON Schema提供了一种强大的方式来定义、存储和验证结构化数据。通过本文介绍的三种转换技术:

  1. JSON Schema到结构化文本
  2. JSON Schema到编辑表格
  3. 表格编辑内容回到JSON Schema

开发者可以构建用户友好的编辑界面,同时保持数据的结构化特性。这种方法特别适合需要复杂结构化数据管理的应用,如内容管理系统、游戏数据编辑器或小说创作辅助工具。

通过这些技术,系统既提供了严格的数据结构定义,又能够以直观的方式呈现给用户进行查看和编辑。