0

I have a websocket which generate different json objects. Objects could contain no any common fields

{
    "type": "apple",
    "kind": "fruit",
    "eatable": true
}
{
    "item": "key",
    "active": true 
}
{
    "tool": "screwdriver",
    "original": "toolBox",
    "cross-head": true
}

I have a list of classes for them (they could contain some logic) so I need to parse it to map some those models with some hierarchical structure e.g. Try to parse fruits if they fails try to parse keys if they fails try to parse toolbox. Sometimes I need to add some new classes to parse some objects and some new fields to existing classes.
How to organize picking class for parsing?

Update

  1. I have no control on backend data so I cannot add any fields to JSON I have.
  2. Objects come one at a time. I have separate class models for most of them. The issue is to choose the right class to map the JSON fields.
4
  • 1
    It is very hard to understand what you're asking and what is happening in your app. Are you receiving one object at a time or do the come in an array or some kind of tree structure and what do you mean that you need to add some classes or fields, how is that relevant to your question? Commented Jan 22, 2019 at 13:50
  • All these objects come in single response i.e. one JSON response object? Commented Jan 22, 2019 at 13:52
  • So are you requesting these objects separately? Because in that case you know beforehand what the response will be and hence what class/struct you will need to decode. Commented Jan 22, 2019 at 14:05
  • 1
    No I don't request any objects they come when they want to via websockets. That is why I don't know what data I got. Commented Jan 22, 2019 at 14:06

3 Answers 3

4

You can do it this way:

First you declare your types conforming to the Decodable protocole:

struct Fruit : Decodable {
    let type : String
    let kind : String
    let eatable : Bool
}

struct Tool : Decodable {
    let tool : String
    let original : String
    let crossHead : Bool

    enum CodingKeys: String, CodingKey {
        case tool = "tool"
        case original = "original"
        case crossHead = "cross-head"
    }
}

Then you extend Decodable to "reverse" the use of the genericity:

extension Decodable {
    static func decode(data : Data, decoder : JSONDecoder = JSONDecoder()) -> Self? {
        return try? decoder.decode(Self.self, from: data)
    }
}

You then extend JSONDecoder to try decodable types among the ones you want to test:

extension JSONDecoder {
    func decode(possibleTypes : [Decodable.Type], from data: Data) -> Any? {
        for type in possibleTypes {
            if let value = type.decode(data: data, decoder: self) {
                return value
            }
        }        
        return nil
    }
}

And eventually you specify the types you want to try and decode:

let decodableTypes : [Decodable.Type] = [Fruit.self, Tool.self]

You can then use it to decode your JSON:

let jsonString = """
    {
        "tool": "screwdriver",
        "original": "toolBox",
        "cross-head": true
    }
    """
let jsonData = jsonString.data(using: .utf8)!

let myUnknownObject = JSONDecoder().decode(possibleTypes: decodableTypes, from: jsonData)

And voilà!!!

Now you can add as much types as you want in your decodableTypes as long as they conform to the Decodable protocol.

It is not the best approach, because if you have many types it won't be optimal, but this way you don't need to add a discriminating field in your data.

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks that's what I looked for. Static extension for Decodable.
decoder : JSONDecoder = JSONDecoder()do not do this, JSONDecoder currently is leaking memory, if you're creating new instance each time you call your extension method you gonna have a ton of leaked memory, better create some static global instance for parameter default value.
This was just to keep the example simple, the idea was to use the JSONDecoder on whichever instance. This code is not meant to be used this way of course. But I used this shortcut to answer the question clearly, and not depict the way JSONDecoder has to be used in production code. And this is true even when there is no leak... Thanks anyway for the reminder.
This is actually a pretty bad idea. If you have 2 structs that share a bunch of common attributes ( e.g. first struct only has Int a, but the second has Int a,b) it's quite possible that decoding to the struct containing Int a works, while the incoming json has Int a, b, so you end up with the wrong object.
4

Try finding the key you are looking for that model class if that key is not present in that object try another model class. This should make you determine which model class is suitable for the given object.

Use the unique key which is not present in any other model class

Example:

var array = NSArray(array: [[
    "type": "apple",
    "kind": "fruit",
    "eatable": true
    ],
    [
        "item": "key",
        "active": true
    ],
    [
    "tool": "screwdriver",
    "original": "toolBox",
    "cross-head": true
    ]])


for model in array as! [NSDictionary]
    {
        if(model.value(forKey: "type") != nil)
        {
            print("use Fruit Model Class")
        }
        else if(model.value(forKey: "item") != nil)
        {
            print("use second model class")
        }
        else
        {
            print("use third model class")
        }
    }

7 Comments

Good decision but I'd like to have some more automatic ways to decide which model to use. Sometimes I need to add a new class to my project so I will have to add some rules here. Perfectly is to have some rule inside the class itself.
Add static functions to each class to check whether the given object contains that unique key which is needed for that Model Class. That will make it lot easier for you to access and use it
I think that is the best you can do to determine the model of that given object.
I think so... Unfortunately.
Don't use NSArray/NSDictionary and value(forKey in Swift. You are fighting the strong type system.
|
1

If all those fields are related or a union style, you may consider user Enum, which is also very easy to implement.

    let  data1 = """
    [{
        "type": "apple",
        "kind": "fruit",
        "eatable": true
    },
    {
        "item": "key",
        "active": true
    },
    {
        "tool": "screwdriver",
        "original": "toolBox",
        "cross-head": true
    }]
    """.data(using: .utf8)!

    struct JSONType : Decodable{
        let type: String
        let kind: String
        let eatable : Bool
    }

    struct JSONItem : Decodable{
        let item: String
        let active : Bool
    }

    struct JSONTool : Decodable{
        let tool: String
        let original : String
        let crosshead : Bool

        enum CodingKeys: String, CodingKey {
            case tool = "tool"
            case original = "original"
            case crosshead = "cross-head"
        }
    }

    enum JSONData : Decodable{

        case type(JSONType)
        case item(JSONItem)
        case tool(JSONTool)

        init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
            do{ let temp = try container.decode(JSONType.self); self = .type(temp) ; return}
            catch{do { let temp = try container.decode(JSONItem.self) ; self = .item(temp) ; return}
            catch{ let temp = try container.decode(JSONTool.self)  ; self = .tool(temp) ; return}}
            try  self.init(from: decoder)
        }

        func getValue()-> Any{
            switch self {
            case let .type(x): return x
            case let .item(x): return x
            case let .tool(x): return x
            }
        }
    }


    let result = try JSONDecoder().decode([JSONData].self, from: data1)
    print(result[0].getValue())
    print (result[1].getValue())
    print (result[2].getValue())

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.