Search code examples
jsonswiftcodabledecodable

Decode JSON single object or array of object dynamically


Let's say I have an array of JSON response from the GET method like :

[{
    "id":"1",
    "Name":"John Doe",
},{
    "id":"2",
    "Name":"Jane Doe",
}]

And from the POST method using id param I only have 1 object JSON response :

{
    "id":"1",
    "Name":"John Doe",
}

how can I write a method to decode both the JSON dynamically? At the moment, this is what I'm using :

func convertJSON<T:Decodable>(result: Any?, model: T.Type) -> T? {
    if let res = result {
        do {
            let data = try JSONSerialization.data(withJSONObject: res, options: JSONSerialization.WritingOptions.prettyPrinted)                
            return try JSONDecoder().decode(model, from: data)
        } catch {
            print(error)
            return nil
        }
    } else {
        return nil
    }
}

The method can be used to decode a single object using dynamic model, but I just can't figure it out to handle a single object / an array of objects dynamically.

The most I can get with is just using a duplicate of the method but replacing T with [T] in the method parameter and return type, if the response is an array.

I'm open to any suggestion, any help is appreciated, Thank You in advance.

Edit : If this question is duplicate of this , I'm not sure how the marked answer could be a solution.


Solution

  • One solution could be to always return [Model]?.

    Inside your function first try to decode as Model, on success return an array with that single decoded object inside it. If this fails then try to decode as [Model], on success return the decoded object else return nil.

    Using your sample JSONs I created a struct:

    struct Person: Codable {
      let id, name: String
    
      enum CodingKeys: String, CodingKey {
        case id
        case name = "Name"
      }
    }
    

    Then I created a struct with a couple of methods to decode from either a String or an optional Data.

    struct Json2Type<T: Decodable> {
      // From data to type T
      static public func convertJson(_ data: Data?) -> [T]? {
        // Check data is not nil
        guard let data = data else { return nil }
        let decoder = JSONDecoder()
        // First try to decode as a single object
        if let singleObject = try? decoder.decode(T.self, from: data) {
          // On success return the single object inside an array
          return [singleObject]
        }
        // Try to decode as multiple objects
        guard let multipleObjects = try? decoder.decode([T].self, from: data) else { return nil }
        return multipleObjects
     }
    
     // Another function to decode from String
     static public func convertJson(_ string: String) -> [T]? {
       return convertJson(string.data(using: .utf8))
     }
    }
    

    Finally call the method you prefer:

    Json2Type<Person>.convertJson(JsonAsDataOrString)
    

    Update: @odin_123, a way to have either a Model or [Model] as return value can be accomplish using an enum. We can even add the error condition there to avoid returning optionals. Let's define the enum as:

    enum SingleMulipleResult<T> {
      case single(T)
      case multiple([T])
      case error
    }
    

    Then the struct changes to something like this:

    struct Json2Type<T: Decodable> {
      static public func convertJson(_ data: Data?) -> SingleMulipleResult<T> {
         guard let data = data else { return .error }
        let decoder = JSONDecoder()
        if let singleObject = try? decoder.decode(T.self, from: data) {
         return .single(singleObject)
        }
         guard let multipleObjects = try? decoder.decode([T].self, from: data) else { return .error }
         return .multiple(multipleObjects)
     }
    
     static public func convertJson(_ string: String) -> SingleMulipleResult<T> {
       return convertJson(string.data(using: .utf8))
     }
    }
    

    You can call it the same way we did before:

    let response = Json2Type<Person>.convertJson(JsonAsDataOrString)
    

    And use a switch to check every possible response value:

    switch(response) {
      case .single(let object):
        print("One value: \(object)")
      case .multiple(let objects):
        print("Multiple values: \(objects)")
      case .error:
        print("Error!!!!")
    }