JavaScript:从一种嵌套 JSON 格式转换为另一种嵌套 JSON 格式

JavaScript: Transform from one nested JSON format to another nested JSON format

我正在尝试将 AVRO 架构转换为 ElasticSearch 索引模板。两者都是 JSON 结构,在转换时需要检查一些内容。我尝试使用递归来取出所有嵌套元素,然后将它们与它们的父元素配对,但是在使用递归进行深度解析时写入字典迫使我问这个问题。

基本上我有这个 AVRO 模式文件:

{
    "name": "animal",
    "type": [
        "null",
        {
            "type": "record",
            "name": "zooAnimals",
            "fields": [{
                    "name": "color",
                    "type": ["null", "string"],
                    "default": null
                },
                {
                    "name": "skinType",
                    "type": ["null", "string"],
                    "default": null
                },
                {
                    "name": "species",
                    "type": {
                        "type": "record",
                        "name": "AnimalSpecies",
                        "fields": [{
                                "name": "terrestrial",
                                "type": "string"
                            },
                            {
                                "name": "aquatic",
                                "type": "string"
                            }
                        ]
                    }
                },
                {
                    "name": "behavior",
                    "type": [
                        "null",
                        {
                            "type": "record",
                            "name": "AnimalBehaviors",
                            "fields": [{
                                    "name": "sound",
                                    "type": ["null", "string"],
                                    "default": null
                                },
                                {
                                    "name": "hunt",
                                    "type": ["null", "string"],
                                    "default": null
                                }
                            ]
                        }
                    ],
                    "default": null
                }
            ]
        }
    ]
}

我希望它能转换成这样(Elasticsearch 索引模板格式):

{
    "properties": {
        "color" :{
            "type" : "keyword"
        },
        "skinType" :{
            "type" : "keyword"
        },
        "species" :{
            "properties" : {
                "terrestrial" : {
                    "type" : "keyword"
                },
                "aquatic" : {
                    "type" : "keyword"
                },
            }
           
        },
        "behavior" : {
            "properties" : {
                "sound" : {
                    "type" : "keyword"
                },
                "hunt" : {
                    "type" : "keyword"
                }
            }
        }
    }
}

重要说明:AVRO 模式上的嵌套可以进一步嵌套,这就是我考虑递归解决的原因。此外,type 字段的类型可以是 ArrayMap,如 behaviorspecies 所示,其中行为具有数组,物种具有一张地图。

如果您一定要看到我做了我的试错,这是我的代码,但我没有任何用处:

const checkDataTypeFromObject = function (obj) {

  if (Object.prototype.toString.call(obj) === "[object Array]") {
    obj.map(function (item) {
      if (Object.prototype.toString.call(item) === "[object Object]") {
        // so this is an object that could contain further nested fields
        dataType = item;
        mappings.properties[item.name] = { "type" : item.type}
         if (item.hasOwnProperty("fields")) {
          checkDataTypeFromObject(item.fields);
        } else if (item.hasOwnProperty("type")) {
          checkDataTypeFromObject(item.type);
        } 
      } else if (item === null) {
        // discard the nulls, nothing to do here
      } else {
        // if not dict or null, this is the dataType we are looking for
        dataType = item;
      }

      return item.name
    });

我不知道你的输入格式,也不知道你的输出格式。所以这可能是不完整的。不过,它捕获了您的示例案例,并且可以作为您可以添加 clauses/conditions:

的基线

const convertField = ({name, type, fields}) =>
  Array .isArray (type) && type [0] === 'null' && type [1] === 'string'
    ? [name, {type: 'keyword'}]
  : Array .isArray (type) && type [0] === 'null' && Object (type [1]) === type [1]
    ? [name, {properties: Object .fromEntries (type [1] .fields .map (convertField))}]
  : Object (type) === type
    ? [name, {properties: Object .fromEntries (type .fields .map (convertField))}]
  : // else 
      [name, {type: 'keyword'}]

const convert = (obj) =>
  convertField (obj) [1]

const input = {name: "animal", type: ["null", {type: "record", name: "zooAnimals", fields: [{name: "color", type: ["null", "string"], default: null}, {name: "skinType", type: ["null", "string"], default: null}, {name: "species", type: {type: "record", name: "AnimalSpecies", fields: [{name: "terrestrial", type: "string"}, {name: "aquatic", type: "string"}]}}, {name: "behavior", type: ["null", {type: "record", name: "AnimalBehaviors", fields: [{name: "sound", type: ["null", "string"], default: null}, {name: "hunt", type: ["null", "string"], default: null}]}], default: null}]}]}

console .log (convert (input))
.as-console-wrapper {min-height: 100% !important; top: 0}

辅助函数 convertField 将输入的一个字段转换为 [name, <something>] 格式,其中 <something>type 属性。在两种情况下,我们使用这些结构的数组作为 Object .fromEntries 的输入以创建一个对象。

主函数 convert 只是从对根调用的 convertField 的结果中获取第二个 属性。如果整体结构总是像本例中那样开始,那将起作用。

请注意,其中两个子句(第一个和第四个)的结果是相同的,另外两个非常相似。此外,对第一条和第二条的测试非常接近。所以可能有一些合理的方法来简化这个。但是因为匹配测试与匹配输出不一致,所以它可能不是微不足道的。

您可以很容易地添加其他条件和结果。事实上,我最初写它的最后两行是这样的:

  : type === 'string'
    ? [name, {type: 'keyword'}]
  : // else 
      [name, {type: 'unknown'}]

它可以更好地显示在何处添加您的其他子句,并且如果您遗漏了一个案例,还会在结果中添加一个符号 (unknown)。

我们可以使用归纳推理来分解它。下面的编号点对应代码中编号的注释 -

  1. 如果输入 t 为空,return 一个空对象
  2. (归纳)t 不为空。如果t.type是一个对象,transform每个叶子和总和成为一个对象
  3. (归纳)t 不为空且 t.type 不是对象。如果t.fields是一个对象,transform每片叶子,赋值给{ [name]: ... },求和成单个properties对象
  4. (归纳)t 不为空,t.type 不是对象,t.fields 不是对象。 Return 关键字.
const transform = t =>
  t === "null"
    ? {}                           // <- 1
: isObject(t.type)
    ? arr(t.type)                  // <- 2
        .map(transform)
        .reduce(assign, {})
: isObject(t.fields)
    ? { propertries:               // <- 3
          arr(t.fields)
            .map(v => ({ [v.name]: transform(v) }))
            .reduce(assign, {})
      }
: { type: "keyword" }              // <- 4

有几个帮手让我们远离复杂性 -

const assign = (t, u) =>
  Object.assign(t, u)

const arr = t =>
  Array.isArray(t) ? t : [t]
  
const isObject = t =>
  Object(t) === t

只需运行 transform -

console.log(transform(input))

展开下面的代码片段以在浏览器中验证结果 -

const assign = (t, u) =>
  Object.assign(t, u)

const arr = t =>
  Array.isArray(t) ? t : [t]
  
const isObject = t =>
  Object(t) === t

const transform = t =>
  t === "null"
    ? {}
: isObject(t.type)
    ? arr(t.type)
        .map(transform)
        .reduce(assign, {})
: isObject(t.fields)
    ? { propertries:
          arr(t.fields)
            .map(v => ({ [v.name]: transform(v) }))
            .reduce(assign, {})
      }
: { type: "keyword" }

const input =
  {name: "animal", type: ["null", {type: "record", name: "zooAnimals", fields: [{name: "color", type: ["null", "string"], default: null}, {name: "skinType", type: ["null", "string"], default: null}, {name: "species", type: {type: "record", name: "AnimalSpecies", fields: [{name: "terrestrial", type: "string"}, {name: "aquatic", type: "string"}]}}, {name: "behavior", type: ["null", {type: "record", name: "AnimalBehaviors", fields: [{name: "sound", type: ["null", "string"], default: null}, {name: "hunt", type: ["null", "string"], default: null}]}], default: null}]}]}

console.log(transform(input))

输出-

{
  "propertries": {
    "color": {
      "type": "keyword"
    },
    "skinType": {
      "type": "keyword"
    },
    "species": {
      "propertries": {
        "terrestrial": {
          "type": "keyword"
        },
        "aquatic": {
          "type": "keyword"
        }
      }
    },
    "behavior": {
      "propertries": {
        "sound": {
          "type": "keyword"
        },
        "hunt": {
          "type": "keyword"
        }
      }
    }
  }
}

注意事项

步骤 2 中,我们可以得到一个复杂的 type,例如 -

{ name: "foo"
, type: [ "null", { obj1 }, { obj2 }, ... ]
, ...
}

在这种情况下,obj1obj2 可能每个 transform 变成一个 { properties: ... } 对象。使用 .reduce(assign, {}) 意味着 obj1 的属性将被 obj2 -

的属性覆盖
: isObject(t.type)
    ? arr(t.type)
        .map(transform)
        .reduce(assign, {})   // <- cannot simply use `assign`

为了解决这个问题,我们将步骤 2 更改为更智能的 merge 复杂类型 -

: isObject(t.type)
    ? arr(t.type)
        .map(transform)
        .reduce(merge, {})   // <- define a more sophisticated merge

其中 merge 可能类似于 -

const merge = (t, u) =>
  t.properties && u.properties // <- both
    ? { properties: Object.assign(t.properties, u.properties) }
: t.properties                 // <- only t
    ? { properties: Object.assign(t.properties, u) }
: u.properties                 // <- only u
    ? { properties: Object.assign(t, u.properties) }
: Object.assign(t, u)          // <- neither

或相同的 merge 但使用不同的逻辑方法 -

const merge = (t, u) =.
  t.properties || u.properties    // <- either
    ? { properties:               
          Object.assign
            ( t.properties || t
            , u.properties || u
            )
      }
    : Object.assign(t, u)         // <- neither