简体   繁体   中英

Using Codable parse Nested JSON like ObjectMapper Swift 4

I have a JSON string, I want to parse it like ObjectMapper Using Codable Protocol.

    struct Health: Mappable {
    var size: [String : Any] = [:]?
    var name: Double?

    init?(map: Map) {

    }

    mutating func mapping(map: Map) {
        size    <- map["health.size"]
        name    <- map["health.name"]
    }
}

I want to eliminate Health struct model with direct access, as creating each model struct for different properties.

let jsonString = """
 {
    "health": {
        "size":{
            "width":150,
            "height":150
            },
        "name":"Apple"
        }
 }
 """ 

I want to access properties with (.) Dot operator like health.size without creating Struct Model for Health.

struct HealthType: Codable {
    var health: Health
 }

struct Health: Codable {
        var title: String
        var size: Size

        enum CodingKeys: String, CodingKey
        {
            case title = "name"
        }
     }

     struct Size: Codable {
        var width: Double
        var height: Double
     }

To do this, you need to implement the Codable protocol yourself. It's not too difficult:

Try the following on Playground.

import Foundation

struct HealthType: Codable {
    let title: String
    let width: Double
    let height: Double

    enum CodingKeys: String, CodingKey
    {
        case health = "health"
        case title = "name"
        case width = "width"
        case height = "height"
        case size = "size"
    }
}

extension HealthType {

    init(from decoder: Decoder) throws {
        let healthTypeContainer = try decoder.container(keyedBy: CodingKeys.self)
        let health = try healthTypeContainer.nestedContainer(keyedBy: CodingKeys.self, forKey: .health)
        let size = try health.nestedContainer(keyedBy: CodingKeys.self, forKey: .size)
        let title = try health.decode(String.self, forKey: .title)
        let width = try size.decode(Double.self, forKey: .width)
        let height = try size.decode(Double.self, forKey: .height)
        self.init(title: title, width: width, height: height)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        var health = container.nestedContainer(keyedBy: CodingKeys.self, forKey: .health)
        var size = health.nestedContainer(keyedBy: CodingKeys.self, forKey: .size)
        try health.encode(title, forKey: .title)
        try size.encode(width, forKey: .width)
        try size.encode(height, forKey: .height)
    }
}

let jsonData = """
{
"health": {
    "size":{
        "width":150,
        "height":150
    },
    "name":"Apple"
}
}
""".data(using: .utf8)!
do {
    print(jsonData)
    let healthType = try JSONDecoder().decode(HealthType.self, from: jsonData)
    print(healthType.title)  // Apple
    print(healthType.width)  // 150.0
    print(healthType.width)  // 150.0
} catch {
    print(error)
}

You can almost make this work. However, JSONDecoder operates on Data instead of String , a Playground is encoded using UTF-8 by default so the following Playground will run:

import Cocoa

let jsonData = """
    {
        "health": {
            "size":{
                "width":150,
                "height":150
           },
           "name":"Apple"
        }
    }
    """.data(using: .utf8)!

struct HealthType: Codable {
    var health: Health
}

struct Health: Codable {
    var title: String
    var size: Size

    enum CodingKeys: String, CodingKey
    {
        case title = "name"
        case size
    }
}

struct Size: Codable {
    var width: Double
    var height: Double
}

do {
    let health = try JSONDecoder().decode(HealthType.self, from:jsonData)
    print(health)
    let h = health.health
    print(h.title)

} catch {
    print(error)
}

While this parses and runs well I cannot make sense of your statement "without creating Struct Model for Health". Part of the tradeoff of using Codable is that you will have to provide structure definitions of the relevant part of your JSON. You can also parse your input into [String:Any].self , but working with it is a drag. You will have to constantly evaluate casts and optionals. When using the Codable protocol for parsing your errors will all be concentrated into the things decode may throw at you. The info you get concerning is fairly good at describing the faults in your JSON (or your struct depending on your point of view).

To cut a long story short: As long as your JSON contains your "health" key you will have to tell it what to do with it.

If your intention is not expose the properties that I don't need then one way is to privately decode the whole structure and then make only those properties available that you are going to expose in the outer world.

struct Health {
    let size: Size
    let title: String

    struct Size: Decodable {
        let width: Int
        let height: Int
    }
    private struct RawResponse: Decodable {
        let health: PrivateHealth
        struct PrivateHealth: Decodable {
            let size: Size
            let name: String
        }
    }
}

// Decodable requirement is moved to extension so that default initializer is accessible
extension Health: Decodable {
    init(from decoder: Decoder) throws {
        let response = try RawResponse(from: decoder)
        size = response.health.size
        title = response.health.name
    }
}

Usage:

let jsonData = """
{
    "health": {
        "size":{
            "width":150,
            "height":150
        },
        "name":"Apple"
    }
}
""".data(using: .utf8)!

do {
    let health = try JSONDecoder().decode(Health.self, from: jsonData)
    print(health.size)  // Size(width: 150, height: 150)
    print(health.title)  // Apple
} catch {
    print(error.localizedDescription)
}

Edit: if you need the encoding feature also

If you also need to implement the Encodable protocol so that you can encode your data like the actual response then you can do it with:

extension Health: Encodable {
    func encode(to encoder: Encoder) throws {
        let health = Health.RawResponse.PrivateHealth(size: size, name: title)
        let response = RawResponse(health: health)
        var container = encoder.singleValueContainer()
        try container.encode(response)
    }
}

For this to work, you will need to make your RawResponse , PrivateHealth and Size struct to be Encodable too

  • Change to private struct RawResponse: Encodable, Decodable {
  • Change to struct PrivateHealth: Encodable, Decodable {
  • Change to struct Size: Encodable, Decodable {

Example of encoding:

let health = Health(size: Health.Size(width: 150, height: 150), title: "Apple")
do {
    let encodedHealth = try JSONEncoder().encode(health)  // Encoded data
    // For checking purpose, you convert the data to string and print
    let jsonString = String(data: encodedHealth, encoding: .utf8)!
    print(jsonString)  // This will ensure data is encoded as your desired format
} catch {
    print(error.localizedDescription)
}

Hey I created KeyedCodable and I think it is exactly what you are looking for. Your implementation will look like this.

struct Health: Codable, Keyedable {
    var size: [String: Int]!
    var name: String?

    mutating func map(map: KeyMap) throws {
        try size <-> map["health.size"]
        try name <-> map["health.name"]
    }

    init(from decoder: Decoder) throws {
        try KeyedDecoder(with: decoder).decode(to: &self)
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM