Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to parse array of polymorphic objects? #8

Closed
rehsals opened this issue Aug 24, 2016 · 4 comments
Closed

How to parse array of polymorphic objects? #8

rehsals opened this issue Aug 24, 2016 · 4 comments
Assignees

Comments

@rehsals
Copy link

rehsals commented Aug 24, 2016

Hi! Thanks for wonderful framework.
I've encountered one issue, I can't figure out.
Let's say I have:

class A {
    let type: String
    /*lots of properties*/
 }

and

final class B: A {
    /*some other properties*/
}

In my response I have JSON-array of objects and some of them turn out to be B's. The type of object determined by type field. Is it possible to use Elevate in this situation? How should I organise my decoding?

@cnoon
Copy link
Member

cnoon commented Aug 24, 2016

Hi @rehsals...it absolutely is possible to use Elevate in this situation!


Problem

Since you have different types inside an Array, you can't just use the decodable convenience as you alluded to. Something like this won't work.

let properties = try Parser.parseProperties(data: data) { make in
make.propertyForKeyPath("objects", type: .Array, decodedToType: A.self)
}

The problem here is that Elevate doesn't know about polymorphism from the JSON object. All it knows is that you want to construct instances of A from each JSON object inside the array.


Solution

Instead, you can use a Decoder to help you "inspect" the data before choosing which class to initialize.

class PolymorphicModelObjectTestCase: XCTestCase {
    func testThatElevateCanUseDecoderToSwitchBetweenPolymorphicModelObjects() {
        do {
            // Given
            let json: AnyObject = [
                [
                    "name": "cnoon"
                ],
                [
                    "name": "rehsals",
                    "sport": "football"
                ]
            ]

            let data = try NSJSONSerialization.dataWithJSONObject(json, options: .PrettyPrinted)

            // When
            let people: [Person] = try Parser.parseArray(data: data, withDecoder: PersonDecoder())

            // Then
            XCTAssertEqual(people.count, 2)

            if people.count == 2 {
                XCTAssertTrue(people[0] is Person)
                XCTAssertTrue(people[1] is Teammate)
            }

            // Show me
            people.forEach { print($0) }
        } catch {
            print("Error occurred: \(error)")
        }
    }
}

class Person: CustomStringConvertible {
    let name: String
    var description: String { return "Person: { \"name\": \"\(name)\" }" }

    init(name: String) {
        self.name = name
    }
}

final class Teammate: Person {
    let sport: String
    override var description: String { return "Teammate: { \"name\": \"\(name)\", \"sport\": \"\(sport)\" }" }

    init(name: String, sport: String) {
        self.sport = sport
        super.init(name: name)
    }
}

class PersonDecoder: Decoder {
    func decodeObject(object: AnyObject) throws -> Any {
        let nameKeyPath = "name"
        let sportKeyPath = "sport"

        let properties = try Parser.parseProperties(json: object) { make in
            make.propertyForKeyPath(nameKeyPath, type: .String)
            make.propertyForKeyPath(sportKeyPath, type: .String, optional: true)
        }

        let name: String = properties <-! nameKeyPath
        let sport: String? = properties <-? sportKeyPath

        if let sport = sport {
            return Teammate(name: name, sport: sport)
        } else {
            return Person(name: name)
        }
    }
}

I'm fairly certain this example should match your use case exactly. @AtomicCat and I came up with a few different approaches, but this is probably the most elegant solution to this particular issue.

Cheers. 🍻

@cnoon cnoon closed this as completed Aug 24, 2016
@cnoon cnoon self-assigned this Aug 24, 2016
@AtomicCat
Copy link

This is eating at my brain...

Alternative approach with fewer optionals:

import Elevate

class A: Decodable {
    let type: String
    let thing: String

    required init(json: AnyObject) throws {
        let typePath = "type"
        let thingPath = "thing"
        let properties = try Parser.parseProperties(json: json) { make in
            make.propertyForKeyPath(typePath, type: .String)
            make.propertyForKeyPath(thingPath, type: .String)
        }

        self.type = properties <-! typePath
        self.thing = properties <-! thingPath
    }
}

final class B: A {
    let other: String

    required init(json: AnyObject) throws {
        let otherPath = "other"

        let properties = try Parser.parseProperties(json: json) { make in
            make.propertyForKeyPath(otherPath, type: .String)
        }

        self.other = properties <-! otherPath
        try super.init(json: json)
    }
}

class PolyDecoder: Decoder {
    func decodeObject(object: AnyObject) throws -> Any {
        let typePath = "type"
        let properties = try Parser.parseProperties(json: object) { make in
            make.propertyForKeyPath(typePath, type: .String)
        }

        let type: String = properties <-! typePath

        if type == "ClassA" {
            return try A(json: object)
        } else if type == "ClassB" {
            return try B(json: object)
        } else {
            throw ParserError.Validation(failureReason: "Unknown type")
        }
    }
}

let json = [
    [ "type": "ClassA", "thing": "foo" ],
    [ "type": "ClassB", "thing": "bar", "other": "whatever" ],
]

do {
    let data = try NSJSONSerialization.dataWithJSONObject(json, options: .PrettyPrinted)
    let objects: [AnyObject] = try Parser.parseArray(data: data, withDecoder: PolyDecoder())

    // Show me
    objects.forEach { print($0) }
} catch {
    print("Error occurred: \(error)")
}

If you don't need B to be a subclass of A, one option would be to put the extra properties that B has in an optional struct in A and handle it in a single decoder.

If they don't need to be classes, you might have additional options by using structs.

@cnoon
Copy link
Member

cnoon commented Aug 24, 2016

Yep, totally a valid solution as well @AtomicCat. @rehsals take your pick. Depending on your actual use case, one may make more sense than the other.

Cheers. 🍻

@rehsals
Copy link
Author

rehsals commented Aug 25, 2016

Great! Thanks for your help, @cnoon. And thanks to @AtomicCat for another approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants