thoughtbot / Argo

Functional JSON parsing library for Swift
https://thoughtbot.com
MIT License
3.49k stars 198 forks source link

Decode type 'Any' #480

Closed Bastosss77 closed 5 years ago

Bastosss77 commented 6 years ago

Hi guys,

I have a JSON with a field could be String, Integer or Float and I don't know how to decode it.

{
   "key": "emission_co2",
   "label": "Emission de CO2",
   "value": 0     <-- Multiple possible types
}

This is a part of my models :

struct DocumentRuleWSModel: BaseWSModel {
    let key: String
    let label: String
    let value: Any?
}

I tried to match it with Any but it doesn't work and I have no idea how to handle this problem

extension DocumentRuleWSModel: Argo.Decodable {
    static func decode(_ json: JSON) -> Decoded<DocumentRuleWSModel> {
        return curry(DocumentRuleWSModel.init)
            <^> json <| "key"
            <*> json <| "label"
            <*> json <|? "value"
    }
}

I'm using Argo 4.1

gfontenot commented 6 years ago

Yeah, decoding directly to Any isn't something we support, and we really couldn't even if we wanted to. In order for decoding to work properly you really need to know the type you're trying to decode to. To make matters worse, you're not going to be able to differentiate between Float and Int values because by the time we get them, they are represented as NSNumber instances. This might change in the future if we ditch Foundation, but for now this is where we are. It's not a great practice to let different types exist under the same JSON key. If you happen to have the ability to change the JSON response I'd definitely recommend standardizing it to return consistent types.

That being said, if I didn't have control over the JSON, I'd probably split out the individual possibilities and then use <|> to set up a kind of precedence chain for them inside the decoder itself:

extension DocumentRuleWSModel: Argo.Decodable {
  static func decode(_ json: JSON) -> Decoded<DocumentRuleWSModel> {
    let stringValue: Decoded<String> = json <| "value"
    let floatValue: Decoded<Float> = json <| "value" // Choosing Float here to reduce the risk of losing precision.

    return curry(DocumentRuleWSModel.init)
      <^> json <| "key"
      <*> json <| "label"
      <*> .optional(stringValue <|> floatValue)
  }
}

Note that the order here is important. <|> will select the first .success value it sees.

Another option might be to wrap your end type in a custom enum type that specifies the types you expect. Something like:

enum DocumentRuleValue {
  case string(String)
  case float(Float)
}

extension DocumentRuleValue: Argo.Decodable {
  static func decode(_ json: JSON) -> Decoded<DocumentRuleValue> {
    switch json {
      case let .string(str): return .string(str)
      case let .number(num): return .float(num.floatValue)
      default: return .typeMismatch(expected: "DocumentRuleValue", actual: json)
    }
  }
}

// then

struct DocumentRuleWSModel: BaseWSModel {
  let key: String
  let label: String
  let value: DocumentRuleValue?
}

extension DocumentRuleWSModel: Argo.Decodable {
  static func decode(_ json: JSON) -> Decoded<DocumentRuleWSModel> {
    return curry(DocumentRuleWSModel.init)
      <^> json <| "key"
      <*> json <| "label"
      <*> json <|? "value"
  }
}
gfontenot commented 5 years ago

I'm going to go ahead and close this due to inactivity but please feel free to reopen if you have any more questions.