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

Comments

Projects
None yet
4 participants
@maxschmeling

maxschmeling commented Feb 9, 2016

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

This comment has been minimized.

Show comment
Hide comment
@maxschmeling

maxschmeling Feb 9, 2016

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 commented Feb 9, 2016

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

This comment has been minimized.

Show comment
Hide comment
@maxschmeling

maxschmeling Feb 9, 2016

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.

maxschmeling commented Feb 9, 2016

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

This comment has been minimized.

Show comment
Hide comment
@paulyoung

paulyoung Feb 9, 2016

Contributor

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

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

Contributor

paulyoung commented Feb 9, 2016

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

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

@maxschmeling

This comment has been minimized.

Show comment
Hide comment

maxschmeling commented Feb 9, 2016

Thanks @paulyoung

@gfontenot

This comment has been minimized.

Show comment
Hide comment
@gfontenot

gfontenot Feb 9, 2016

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) }
Collaborator

gfontenot commented Feb 9, 2016

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

This comment has been minimized.

Show comment
Hide comment
@maxschmeling

maxschmeling Feb 9, 2016

@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 commented Feb 9, 2016

@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

This comment has been minimized.

Show comment
Hide comment
@maxschmeling

maxschmeling Feb 9, 2016

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.

maxschmeling commented Feb 9, 2016

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

This comment has been minimized.

Show comment
Hide comment
@gfontenot

gfontenot Feb 9, 2016

Collaborator

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"
}
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

This comment has been minimized.

Show comment
Hide comment
@gfontenot

gfontenot Feb 9, 2016

Collaborator

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

Collaborator

gfontenot commented Feb 9, 2016

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

@maxschmeling

This comment has been minimized.

Show comment
Hide comment
@maxschmeling

maxschmeling Feb 9, 2016

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

maxschmeling commented Feb 9, 2016

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

@maxschmeling

This comment has been minimized.

Show comment
Hide comment
@maxschmeling

maxschmeling Feb 9, 2016

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.

maxschmeling commented Feb 9, 2016

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

This comment has been minimized.

Show comment
Hide comment
@gfontenot

gfontenot Feb 9, 2016

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.

Collaborator

gfontenot commented Feb 9, 2016

@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

This comment has been minimized.

Show comment
Hide comment
@maxschmeling

maxschmeling Feb 10, 2016

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

maxschmeling commented Feb 10, 2016

@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

This comment has been minimized.

Show comment
Hide comment
@hunaid-hassan-confiz

hunaid-hassan-confiz 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

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

This comment has been minimized.

Show comment
Hide comment
@gfontenot

gfontenot Apr 28, 2016

Collaborator

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

Collaborator

gfontenot commented Apr 28, 2016

@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

This comment has been minimized.

Show comment
Hide comment
@hunaid-hassan-confiz

hunaid-hassan-confiz Apr 29, 2016

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

hunaid-hassan-confiz commented Apr 29, 2016

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

This comment has been minimized.

Show comment
Hide comment
@gfontenot

gfontenot Apr 29, 2016

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.

Collaborator

gfontenot commented Apr 29, 2016

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

This comment has been minimized.

Show comment
Hide comment
@hunaid-hassan-confiz

hunaid-hassan-confiz May 2, 2016

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 :)

hunaid-hassan-confiz commented May 2, 2016

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