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

Is there any way to decode an array of different types? #325

Closed
maxschmeling opened this issue Feb 9, 2016 · 18 comments
Closed

Is there any way to decode an array of different types? #325

maxschmeling opened this issue Feb 9, 2016 · 18 comments

Comments

@maxschmeling
Copy link

I have JSON that is analogous to this:

{
  "animals": [
    { "type": "cat", "name": "kitty", "remainingLives": 4 },
    { "type": "dog", "name": "spot", "trained": false }
  ]
}

I would love to have an Animal type with a name property and then a subclass for Cat with a remainingLives property and Dog with a trained property.

It doesn't appear to be possible with Argo. Am I missing anything? If not, is this something that you think might fit into Argo? I would be happy to work on it with a little guidance. (I'm very new to Swift)

@maxschmeling
Copy link
Author

I realized I can do something like this:

extension Animal: Decodable {
    static func decode(j: JSON) -> Decoded<Animal> {
        let dict = j.JSONObject() as! [String:AnyObject]
        let type = dict["type"] as! String

        if type == "cat" {
            return Cat.decode(j)
        } else if type == "dog" {
            return Dog.decode(j)
        }

        return curry(Animal.init)
            <^> j <| "name"
    }
}

Is this the most appropriate thing to do in this scenario?

@maxschmeling
Copy link
Author

I guess this won't actually work because the Swift compiler can't convert Decoded<Cat> to Decoded<Animal>. I feel like I'm just missing something simple because of my inexperience with the language.

@paulyoung
Copy link
Contributor

@maxschmeling I think you'd need to use type erasure to allow that.

This article by @rnapier might help: http://robnapier.net/erasure

@maxschmeling
Copy link
Author

Thanks @paulyoung

@gfontenot
Copy link
Collaborator

I don't think you'll need to go as far as type erasure, instead you should be able to use <|> to flip between the different subclasses (kinda turning Animal into a class cluster). I might try something like this:

class Animal {
  let name: String

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

extension Animal: Decodable {
  static func decode(j: JSON) -> Decoded<Animal> {
    return Cat.decode(j) <|> Dog.decode(j)
  }
}

final class Cat: Animal {
  let remainingLives: Int

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

extension Cat { // Note that I'm _not_ making Cat decodable
  static func decode(j: JSON) -> Decoded<Animal> {
    let cat = curry(self.init)
      <^> j <| "name"
      <*> j <| "remainingLives"

    return cat as Decoded<Animal> // This might not be needed, I might try just returning the expression above first
  }
}

final class Dog: Animal {
  let trained: Bool

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

extension Dog { // Note that I'm _not_ making Dog decodable
  static func decode(j: JSON) -> Decoded<Animal> {
    let dog = curry(self.init)
      <^> j <| "name"
      <*> j <| "trained"

    return dog as Decoded<Animal> // This might not be needed, I might try just returning the expression above first
  }
}

Since the types for the Cat and Dog initializers is different, this should be all you need. If there are overlapping animal subclasses, you could add a simple validator to the beginning of your subclass decode functions:

func validateType(expectedType: String, json: JSON) -> Bool {
  let type: String? = (json <| "type").value

  return type == expectedType
}

// later, in Dog.decode:

guard validateType("dog") else { return .typeMismatch("dog", actual: j) }

@maxschmeling
Copy link
Author

@gfontenot maybe I'm doing something wrong, but the compiler says:

Cannot convert return expression of type 'Decoded<Dog>' to return type 'Decoded<Animal>'.

on return dog as Decoded<Animal>

@maxschmeling
Copy link
Author

I'm also having trouble getting the type erasure working, but still working on it. Trying to wrap my head around how it would work in this scenario.

@gfontenot
Copy link
Collaborator

gfontenot commented Feb 9, 2016

Sorry, I had spiked this code out here on GitHub. I was able to get this to compile by:

  • Removing the Cat and Dog decode functions in favor of global functions that performed the same. This removed an error about ambiguous uses of decode. It also removed an error about how you can't override functions in extensions.
  • Removing the cast to Decoded<Animal>, instead just returning the result of the parser expression.

The resulting code (that compiles for me):

class Animal {
  let name: String

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

extension Animal: Decodable {
  static func decode(j: JSON) -> Decoded<Animal> {
    return decodeCat(j) <|> decodeDog(j)
  }
}

final class Cat: Animal {
  let remainingLives: Int

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

func decodeCat(j: JSON) -> Decoded<Animal> {
  return curry(Cat.init)
    <^> j <| "name"
    <*> j <| "remainingLives"
}

final class Dog: Animal {
  let trained: Bool

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

func decodeDog(j: JSON) -> Decoded<Animal> {
  return curry(Dog.init)
    <^> j <| "name"
    <*> j <| "trained"
}

@gfontenot
Copy link
Collaborator

Type erasure really shouldn't be needed here, because we can use covariance to return Decoded<Cat> as Decoded<Animal>.

@maxschmeling
Copy link
Author

@gfontenot the compiler errors were making me think that isn't possible. Working on attempting the latest code sample. I appreciate the help.

@maxschmeling
Copy link
Author

I was able to convert this into my real-world code and it's compiling. Will do some testing to make sure it's all working, but it appears to do what I need.

I really appreciate the help.

@gfontenot
Copy link
Collaborator

@maxschmeling Glad it's working for you! Don't hesitate to re-open if you need more help or if you have any other questions.

@maxschmeling
Copy link
Author

@gfontenot just wanted to follow up and say that once I added in the typeMismatch guard, everything worked perfectly. Thanks again for the help.

@hunaid-hassan-confiz
Copy link

hunaid-hassan-confiz commented Apr 28, 2016

how can I go about implementing this with a fairly large model? you know, the type where we have to split curry in intermediate vars to avoid "Expression too complex" errors.
This is my model

func decodeCar(j: JSON) -> Decoded<AdDetailModel> {

    let a = curry(CarAdDetailModel.init)
        <^> j <| "ad_id"
        <*> j <| "phone"
        <*> j <| "city_name"
        <*> j <| "city_area"
        <*> j <| "seller_comments"
        <*> j <| "price"
        <*> j <| "url_slug"
        <*> j <|| "pictures"
        <*> j <| "is_featured"
        <*> j <| "ad_listing_id"
    return a
        <*> (j <| "last_updated" >>- toNSDate(""))
        <*> j <| "featured_request"
        <*> j <| "ad_saved"
        <*> j <| "make"
        <*> j <| "model"
        <*> j <| "version"
}

There is a return type mismatch compiler error

@gfontenot
Copy link
Collaborator

@hunaid-hassan-confiz I'm not sure what you're asking. Can I see your model? Also probably the implementation of toNSDate?

@hunaid-hassan-confiz
Copy link

Nevermind. My model is unusually large. 25+ properties. I was having trouble getting rid of the compiler error "Expression too complex", which I managed to somehow but then having to write an initialiser for every class explicitly was too much of work also Argo hits its limit at 15+ properties after which the compiler error just doesn't go away whatever you do. So I am planning to move to alternative. I haven't decided which though

@gfontenot
Copy link
Collaborator

Argo hits its limit at 15+ properties after which the compiler error just doesn't go away whatever you do.

To be clear, Argo has zero limits on the number of properties. I believe what you're hitting is the limitations (imposed by the compiler) on the number of curry instances we can provide via Curry.framework. We recently updated Curry.framework to be able to handle up to 21 arguments, although that won't help you here given that your model has 25+.

TBH, I constantly question the design of models that have that many properties. I don't know the domain you're working in at all, but I'd highly suspect that you could refactor into multiple objects which might improve the overall design as well as make it easier to use with Argo.

@hunaid-hassan-confiz
Copy link

You are right. I am not very happy with the current design of the models. A lot of those properties can be grouped together in smaller structs but its a legacy system I have to work with.
It indeed is a compiler limitation. Hope it goes away soon. Thanks for the time though :)

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

No branches or pull requests

4 participants