Home > Enterprise >  Decode different incoming JSON types in Swift
Decode different incoming JSON types in Swift

Time:09-13

I'm using JSONDecoder to decode incoming websocket messages from an API. Messages come from the websockettask as a String. Right now I have my Codable struct as such:

struct JsonRPCMessage: Codable {
    let jsonrpc: String
    let result: String?
    let method: String?
    let id: Int?
}

Then I just decode it like:

let message = try decoder.decode(JsonRPCMessage.self, from: data!)

This has worked fine for about half of the endpoints in the API which just return a single String for result. The others return a dictionary. When I change the type of result to Dictionary, the struct no longer conforms to Codable. When it's left as a string, the decoder returns a type mismatch error at runtime. Plus, changing the type to dictionary would break functionality for the rest of the api's features.

Looking for ideas to decode and access the string to value pairs in that dictionary as well as check for dictionary or string before sending it to the decoder.

Here are some samples of the different types of response I need to be able to sort and parse:

{
  "jsonrpc": "2.0",
  "result": {
    "klippy_connected": true,
    "klippy_state": "ready",
    "components": [
      "klippy_connection",
      "history",
      "octoprint_compat",
      "update_manager"
    ],
    "failed_components": [],
    "registered_directories": [
      "config",
      "logs",
      "gcodes",
      "config_examples",
      "docs"
    ],
    "warnings": [],
    "websocket_count": 4,
    "moonraker_version": "v0.7.1-659-gf047167",
    "missing_klippy_requirements": [],
    "api_version": [1, 0, 5],
    "api_version_string": "1.0.5"
  },
  "id": 50
}
{
  "jsonrpc": "2.0",
  "method": "notify_proc_stat_update",
  "params": [
    {
      "moonraker_stats": {
        "time": 1663016434.5099802,
        "cpu_usage": 0.74,
        "memory": 35716,
        "mem_units": "kB"
      },
      "cpu_temp": null,
      "network": {
        "lo": { "rx_bytes": 2568, "tx_bytes": 2568, "bandwidth": 0.0 },
        "tunl0": { "rx_bytes": 0, "tx_bytes": 0, "bandwidth": 0.0 },
        "ip6tnl0": { "rx_bytes": 0, "tx_bytes": 0, "bandwidth": 0.0 },
        "eth0": {
          "rx_bytes": 2529302,
          "tx_bytes": 13891023,
          "bandwidth": 7005.14
        }
      },
      "system_cpu_usage": {
        "cpu": 25.62,
        "cpu0": 1.98,
        "cpu1": 1.0,
        "cpu2": 0.0,
        "cpu3": 100.0
      },
      "system_memory": {
        "total": 8039920,
        "available": 7182640,
        "used": 857280
      },
      "websocket_connections": 4
    }
  ]
}
{ 
    "jsonrpc": "2.0", 
    "result": "ok", 
    "id": 50 
}

CodePudding user response:

In this case, the better option is to receive the same JSON in each case, but if you can't control that then you can implement custom decoding using init(from:).

struct JsonRPCMessage: Decodable {
    enum CodingKeys: String, CodingKey {
        case jsonrpc, result, method, id
    }
    
    let jsonrpc: String
    let result: String?
    let method: String?
    let id: Int?
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.jsonrpc = try container.decode(String.self, forKey: .jsonrpc)            
        if let dic = try container.decodeIfPresent([String: Any].self, forKey: .result) {
            self.result = dic["YourKey"] as? String
        }
        else {
            self.result = try container.decodeIfPresent(String.self, forKey: .result)
        }
        // If you have a custom type for Result than
        if let result = try container.decodeIfPresent(YourResultType.self, forKey: .result) {
            self.result = result.propertyOfYourResult
        }
        else {
            self.result = try container.decodeIfPresent(String.self, forKey: .result)
        }
        self.method = try container.decodeIfPresent(String.self, forKey: .method)
        self.id = try container.decodeIfPresent(Int.self, forKey: .id)
    }
} 

CodePudding user response:

You're trying to force 3 different JSON shapes into the same Swift struct. This is generally not advised. You could do custom decoding as @Nirav suggested, but then you're essentially adding business logic decoding to your models and this will quickly grow out of hand and be untestable.

I believe a far better solution is to create 3 different struct and try to decode one, if not, try decoding the other, etc... and handle any error as appropriate, and test this behaviour:

import Foundation
import SwiftUI

let json1 = """
{
  "jsonrpc": "2.0",
  "result": {
    "klippy_connected": true,
    "klippy_state": "ready",
    "components": [
      "klippy_connection",
      "history",
      "octoprint_compat",
      "update_manager"
    ],
    "failed_components": [],
    "registered_directories": [
      "config",
      "logs",
      "gcodes",
      "config_examples",
      "docs"
    ],
    "warnings": [],
    "websocket_count": 4,
    "moonraker_version": "v0.7.1-659-gf047167",
    "missing_klippy_requirements": [],
    "api_version": [1, 0, 5],
    "api_version_string": "1.0.5"
  },
  "id": 50
}
""".data(using: .utf8)!

struct JSON1: Codable {
    var jsonrpc: String
    var result: JSONResult
    var id: Int

    struct JSONResult: Codable {
        var klippy_connected: Bool
        var klippy_state: String
        var components: [String]
        var failed_components: [String]
        var registered_directories: [String]
        // etc...
    }
}

let json2 = """
{
  "jsonrpc": "2.0",
  "method": "notify_proc_stat_update",
  "params": [
    {
      "moonraker_stats": {
        "time": 1663016434.5099802,
        "cpu_usage": 0.74,
        "memory": 35716,
        "mem_units": "kB"
      },
      "cpu_temp": null,
      "network": {
        "lo": { "rx_bytes": 2568, "tx_bytes": 2568, "bandwidth": 0.0 },
        "tunl0": { "rx_bytes": 0, "tx_bytes": 0, "bandwidth": 0.0 },
        "ip6tnl0": { "rx_bytes": 0, "tx_bytes": 0, "bandwidth": 0.0 },
        "eth0": {
          "rx_bytes": 2529302,
          "tx_bytes": 13891023,
          "bandwidth": 7005.14
        }
      },
      "system_cpu_usage": {
        "cpu": 25.62,
        "cpu0": 1.98,
        "cpu1": 1.0,
        "cpu2": 0.0,
        "cpu3": 100.0
      },
      "system_memory": {
        "total": 8039920,
        "available": 7182640,
        "used": 857280
      },
      "websocket_connections": 4
    }
  ]
}
""".data(using: .utf8)!

struct JSON2: Codable {
    var jsonrpc: String
    var params: [JSONParams]
    var method: String

    struct JSONParams: Codable {
        var moonraker_stats: MoonrakerStats
        // etc...

        struct MoonrakerStats: Codable {
            var time: Double
            var cpu_usage: Double
            // etc...
        }
    }
}

let json3 = """
{
    "jsonrpc": "2.0",
    "result": "ok",
    "id": 50
}
""".data(using: .utf8)!

struct JSON3: Codable {
    var jsonrpc: String
    var result: String
    var id: Int
}


let data = [json1, json2, json3].randomElement()!
let decoder = JSONDecoder()

if let decoded = try? decoder.decode(JSON1.self, from: data) {
    print("we have json 1")
    print(decoded)
} else if let decoded = try? decoder.decode(JSON2.self, from: data) {
    print("we have json 2")
    print(decoded)
} else if let decoded = try? decoder.decode(JSON3.self, from: data) {
    print("we have json 3")
    print(decoded)
} else {
    print("we don't know what we have")
}
  • Related