swift 使用Codable 解析复杂类型

项目上有一个需求,把一个h5页面改造为原生app,但接口和json仍使用原来的。
但json的格式对iOS非常不友好
它的结构可能是这样的

{
    "A": "呵呵",
    "B": "哈哈",
    "C": "CCC",
    "路人甲": "123"
}

这样的

{
    "A": "呵呵",
    "B": ["哈哈"],
    "C": {
        "height": "10cm",
        "width": "5cm"
    },
    "路人甲": {
        "info": {
            "年龄": 18,
            "性别": "男"
        }
    }
}

甚至这样的

{
    "A": "呵呵",
    "B": ["哈哈"],
    "C": {
        "height": "10cm",
        "width": "5cm"
    },
    "路人甲": {
        "info": {
            "年龄": 48,
            "性别": "男"
        },
        "children": {
            "路人乙": {
                "info": {
                    "年龄": 22,
                    "性别": "男"
                }
            },
            "路人丙": {
                "info": {
                    "年龄": 28,
                    "性别": "女"
                },
                "children": {
                    "小明": {
                        "info": {
                            "年龄": 4,
                            "性别": "男"
                        }
                    }
                }
            }
        }
    }
}

孩子那个看不懂?可以看看下面这个,也是类似的结构。

{
    "一级菜单": {
        "code": "1",
        "data": {
            "二级菜单1": {
                "code": "100",
                "data": {
                    "三级菜单11": {
                        "code": "10000"
                    }
                }
            },
            "二级菜单2": {
                "code": "101",
                "data": {
                    "三级菜单21": {
                        "code": "10100"
                    },
                    "三级菜单22": {
                        "code": "10101"
                    }
                }
            }
        }
    }
}

数了数,有好几个坑。

  1. 字典的key带有中文
  2. 字典的value类型不确定,可能是int, 可能是string,可能是数组,还有可能又是一个同样的字典,这个结构也许可以无限循环下去。
  3. 因为需求需要根据这个json来展示分级菜单,但菜单名存储在key里面,并且同级菜单不是存在一个数组里。
    其实这个最好的解决方案是在字典里加一个name字段,然后把原来的字典改成数组。(无奈)

当然我看到这个问题首先肯定是去问领导,这个能不能让后端改一下json
然而被否决了。
而且所有的数据解析都要求用Model来处理,还不让直接用字典
这个字典转模型难度实在是有点大。

但是最终还是解决了

下面步骤:

  1. key带中文问题
    用JSONDecoder把字典转model,字典的key必须和model的属性一样,而属性不能用中文
    但我们可以使用CodingKeys更改他们的对应关系。
struct AModel: Codable {
    var A: String?
    var B: String?
    var C: String?
    var someone: String?
    
    enum CodingKeys: String, CodingKey {
        case someone = "路人甲"
        case A
        case B
        case C
    }
}

这样在转换的时候 路人甲的数据就可以存到someone里去了

  1. value类型不确定
    这个我们可以使用枚举的高级用法——关联值(Associated Value)
    先上代码
enum TestModelEnum: Codable {
    case int(Int)
    case string(String)
    case stringArray([String])
    case dictionary([String: String])
    case modelDict([String: TestModelEnum])

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let x = try? container.decode(Int.self) {
            self = .int(x)
            return
        }

        if let x = try? container.decode(String.self) {
            self = .string(x)
            return
        }

        if let x = try? container.decode([String].self) {
            self = .stringArray(x)
            return
        }

        if let x = try? container.decode([String: String].self) {
            self = .dictionary(x)
            return
        }

        if let x = try? container.decode([String: TestModelEnum].self) {
            self = .modelDict(x)
            return
        }

        throw DecodingError.typeMismatch(TestModelEnum.self,
                                         DecodingError.Context(codingPath: decoder.codingPath,
                                                               debugDescription: "Wrong type for TestModelEnum"))
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .int(let x):
            try container.encode(x)
        case .string(let x):
            try container.encode(x)
        case .stringArray(let x):
            try container.encode(x)
        case .dictionary(let x):
            try container.encode(x)
        case .modelDict(let x):
            try container.encode(x)
        }
    }
}

我们把所有可能出现的类型都写在了case里,而且由于字典里还可能再嵌套字典,所以我的枚举类型里包含了自身
在init方法中会去尝试转换成各种类型,转换失败会去尝试另一种,需要注意的是,如果你的类型是另一个Model, 而且这个Model的所有属性都是可选的(optional),但实际上你这里的数据可能只是一个String,转换会成功但Model里面的值都为nil【一定要失败才能继续往下进行,可以控制优先级或者去掉optional】

  1. 不止要转换成功,还要把key作为信息保存起来
    先从那个菜单的开始,那个结构相对简单一点
    我们先写一个基本类型,这是每级菜单里的信息
struct MenuModel: Codable {
    var code: String?
    var data: [String: MenuModel]?
}

再写一个AllMenuModel用来转存MenuModel的数据

struct AllMenuModel: Codable {
    var values: [MenuModel] = []
    var keys: [String] = []

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let x = try? container.decode([String: MenuModel].self) {
            for item in x {
                keys.append(item.key)
                values.append(item.value)
            }
            return
        }

        throw DecodingError.typeMismatch(MenuModel.self,
                                         DecodingError.Context(codingPath: decoder.codingPath,
                                                               debugDescription: "Wrong type for MenuModel"))
    }
}

测试方法

    func test() {
        let json = [
            "一级菜单": [
                "code": "1",
                "data": [
                    "二级菜单1": [
                        "code": "100",
                        "data": [
                            "三级菜单11": [
                                "code": "10000"
                            ]
                        ]
                    ],
                    "二级菜单2": [
                        "code": "101",
                        "data": [
                            "三级菜单21": [
                                "code": "10100"
                            ],
                            "三级菜单22": [
                                "code": "10101"
                            ]
                        ]
                    ]
                ]
            ]
        ] as [String: Any]
        let model = try? JSONDecoder().decode(AllMenuModel.self, from: JSONSerialization.data(withJSONObject: json, options: []))
        print(model)
    }

输出结果:

Optional(DecoderTest.AllMenuModel(values: [DecoderTest.MenuModel(code: Optional("1"), data: Optional(["二级菜单2": DecoderTest.MenuModel(code: Optional("101"), data: Optional(["三级菜单22": DecoderTest.MenuModel(code: Optional("10101"), data: nil), "三级菜单21": DecoderTest.MenuModel(code: Optional("10100"), data: nil)])), "二级菜单1": DecoderTest.MenuModel(code: Optional("100"), data: Optional(["三级菜单11": DecoderTest.MenuModel(code: Optional("10000"), data: nil)]))]))], keys: ["一级菜单"]))

转换成功了,并且key被我们存到了AllMenuModel里以供使用。
当然,也可以写一个BaseModel基类,里面带一个属性key,这样key和value可以在同一个model里。
我这里就不写了。

这个问题解决了,和第二个解决方案组合一下就可以解决路人甲的那个问题了。
但是要解决那个问题,又得写一个类似AllMenuModel的结构体,会不会太麻烦了。
于是我对这里的代码做了一些改进。

其实他们的区别只在于values的类型不一样,把类型做成泛型就好了。

public struct KeyValueDictionary<T: Codable>: Codable {
    var values: [T] = []
    var keys: [String] = []

    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let x = try? container.decode([String: T].self) {
            for item in x {
                keys.append(item.key)
                values.append(item.value)
            }
            return
        }

        throw DecodingError.typeMismatch(KeyValueDictionary.self,
                                         DecodingError.Context(codingPath: decoder.codingPath,
                                                               debugDescription: "Wrong type for KeyValueDictionary"))
    }
}

enum TestModelEnum: Codable {
    case int(Int)
    case string(String)
    case stringArray([String])
    case dictionary([String: String])
    case info(KeyValueDictionary<[String: TestModelEnum]>)

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let x = try? container.decode(Int.self) {
            self = .int(x)
            return
        }

        if let x = try? container.decode(String.self) {
            self = .string(x)
            return
        }

        if let x = try? container.decode([String].self) {
            self = .stringArray(x)
            return
        }

        if let x = try? container.decode([String: String].self) {
            self = .dictionary(x)
            return
        }

        if let x = try? container.decode(KeyValueDictionary<[String: TestModelEnum]>.self) {
            self = .info(x)
            return
        }

        throw DecodingError.typeMismatch(TestModelEnum.self,
                                         DecodingError.Context(codingPath: decoder.codingPath,
                                                               debugDescription: "Wrong type for TestModelEnum"))
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .int(let x):
            try container.encode(x)
        case .string(let x):
            try container.encode(x)
        case .stringArray(let x):
            try container.encode(x)
        case .dictionary(let x):
            try container.encode(x)
        case .info(let x):
            try container.encode(x)
        }
    }
}

这样,上面提到的坑全部都解决了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,204评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,091评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,548评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,657评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,689评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,554评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,302评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,216评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,661评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,851评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,977评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,697评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,306评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,898评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,019评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,138评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,927评论 2 355

推荐阅读更多精彩内容