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

Fatal error: Optional is only JSONEncodable if Wrapped is #1255

Closed
odanu opened this issue Jun 11, 2020 · 10 comments · Fixed by #1262
Closed

Fatal error: Optional is only JSONEncodable if Wrapped is #1255

odanu opened this issue Jun 11, 2020 · 10 comments · Fixed by #1262
Labels
caching crash Issues which cause crashes
Milestone

Comments

@odanu
Copy link

odanu commented Jun 11, 2020

Hey hey.
Getting this error:

Fatal error: Optional is only JSONEncodable if Wrapped is: file .../apollo-ios/Sources/Apollo/JSONStandardTypeConversions.swift, line 109

Query which creates the situation:

query UncategorizedTransactions($fromDate: Timestamp!, $toDate: Timestamp!) {
  me {
    transactionsConnection(first: 0, filters: {
      fromDate: $fromDate,
      toDate: $toDate,
      categoryId: null
    }) {
      totalCount
    }
  }
}

Example of playground response

{
  "data": {
    "me": {
      "transactionsConnection": {
        "totalCount": 0
      }
    }
  },
  "extensions": {
    "cacheControl": {
      "version": 1,
      "hints": [
        {
          "path": [
            "me"
          ],
          "maxAge": 0
        },
        {
          "path": [
            "me",
            "transactionsConnection"
          ],
          "maxAge": 0
        }
      ]
    }
  }
}

It doesn't crash if the query is like so:

query UncategorizedTransactions {
  me {
    transactionsConnection(first: 0, filters: {
      fromDate: 1524379940,
      toDate: 1524379940,
      categoryId: null
    }) {
      totalCount
    }
  }
}

Also it doesn't crash if the query is this:

query UncategorizedTransactions($dateFrom: Timestamp!, $dateTo: Timestamp!) {
  me {
    transactionsConnection(first: 0, filters: {
      fromDate: $dateFrom,
      toDate: $dateTo,
      categoryId: "someStringValue"
    }) {
      totalCount
    }
  }
}
@designatednerd
Copy link
Contributor

Can you share what the generated code is for this query, please? I'm mostly curious to see what type totalCount is - Int is most certainly JSONEncodable so I'm not sure why you'd be getting that crash.

@odanu
Copy link
Author

odanu commented Jun 11, 2020

It's an Int ```

public let operationName: String = "UncategorizedTransactions"

public var fromDate: Timestamp
public var toDate: Timestamp

public init(fromDate: Timestamp, toDate: Timestamp) {
self.fromDate = fromDate
self.toDate = toDate
}

public var variables: GraphQLMap? {
return ["fromDate": fromDate, "toDate": toDate]
}

public struct Data: GraphQLSelectionSet {
public static let possibleTypes: [String] = ["Query"]

public static let selections: [GraphQLSelection] = [
  GraphQLField("me", type: .object(Me.selections)),
]

public private(set) var resultMap: ResultMap

public init(unsafeResultMap: ResultMap) {
  self.resultMap = unsafeResultMap
}

public init(me: Me? = nil) {
  self.init(unsafeResultMap: ["__typename": "Query", "me": me.flatMap { (value: Me) -> ResultMap in value.resultMap }])
}

public var me: Me? {
  get {
    return (resultMap["me"] as? ResultMap).flatMap { Me(unsafeResultMap: $0) }
  }
  set {
    resultMap.updateValue(newValue?.resultMap, forKey: "me")
  }
}

public struct Me: GraphQLSelectionSet {
  public static let possibleTypes: [String] = ["User"]

  public static let selections: [GraphQLSelection] = [
    GraphQLField("__typename", type: .nonNull(.scalar(String.self))),
    GraphQLField("transactionsConnection", arguments: ["first": 0, "filters": ["fromDate": GraphQLVariable("fromDate"), "toDate": GraphQLVariable("toDate"), "categoryId": nil]], type: .nonNull(.object(TransactionsConnection.selections))),
  ]

  public private(set) var resultMap: ResultMap

  public init(unsafeResultMap: ResultMap) {
    self.resultMap = unsafeResultMap
  }

  public init(transactionsConnection: TransactionsConnection) {
    self.init(unsafeResultMap: ["__typename": "User", "transactionsConnection": transactionsConnection.resultMap])
  }

  public var __typename: String {
    get {
      return resultMap["__typename"]! as! String
    }
    set {
      resultMap.updateValue(newValue, forKey: "__typename")
    }
  }

  /// Get user transactions
  public var transactionsConnection: TransactionsConnection {
    get {
      return TransactionsConnection(unsafeResultMap: resultMap["transactionsConnection"]! as! ResultMap)
    }
    set {
      resultMap.updateValue(newValue.resultMap, forKey: "transactionsConnection")
    }
  }

  public struct TransactionsConnection: GraphQLSelectionSet {
    public static let possibleTypes: [String] = ["TransactionConnection"]

    public static let selections: [GraphQLSelection] = [
      GraphQLField("__typename", type: .nonNull(.scalar(String.self))),
      GraphQLField("totalCount", type: .nonNull(.scalar(Int.self))),
    ]

    public private(set) var resultMap: ResultMap

    public init(unsafeResultMap: ResultMap) {
      self.resultMap = unsafeResultMap
    }

    public init(totalCount: Int) {
      self.init(unsafeResultMap: ["__typename": "TransactionConnection", "totalCount": totalCount])
    }

    public var __typename: String {
      get {
        return resultMap["__typename"]! as! String
      }
      set {
        resultMap.updateValue(newValue, forKey: "__typename")
      }
    }

    public var totalCount: Int {
      get {
        return resultMap["totalCount"]! as! Int
      }
      set {
        resultMap.updateValue(newValue, forKey: "totalCount")
      }
    }
  }
}

}
}

</details>

@designatednerd
Copy link
Contributor

Interesting - the only thing in there being generated as an optional is Me, and it doesn't seem like there's any reason that should be failing.

Is there a chance you could either share your project with me or get me a project that reproduces this in isolation? I'd really need to dig in a bit here to figure out what's going on. Email ellen at apollographql dot com.

@designatednerd designatednerd added the crash Issues which cause crashes label Jun 11, 2020
@odanu odanu changed the title Fatal error: Optional is only JSONEncodable if Wrapped is: file Fatal error: Optional is only JSONEncodable if Wrapped is Jun 11, 2020
@designatednerd
Copy link
Contributor

OK! So after some wrangling and switching from Carthage to SPM I was able to see what's causing this crash: When the code to compute the default cache key is getting hit, the optional it's trying to unwrap is a GraphQLVariable instead of the actual Timestamp type:

Screen Shot 2020-06-15 at 2 26 13 PM

GraphQLVariable indeed does not conform to JSONEncodable, so the whole thing goes 💥.

This is extremely deep in our caching mechanism and I'm going to have to ping @martijnwalraven for some help on how to debug and fix this since he wrote this code and understands it a LOT better than I do.

In the meantime, I would recommend using the .fetchIgnoringCacheCompletely cache policy for this particular query (and anything else that uses your Timestamp scalar as a parameter) until I can figure out what the hell is going on here.

martijnwalraven added a commit to martijnwalraven/apollo-ios that referenced this issue Jun 17, 2020
This fixes apollographql#1255 by adding an extension method to `Optional` that implements `GraphQLInputValue.evaluate(with:)` by invoking that same method on the wrapped value. Without it, we'd be treating an optional `GraphQLVariable` as an optional `JSONEncodable`, which leads to a runtime crash because `GraphQLVariable` does not in fact conform to `JSONEncodable`.

All this should really be cleaned up by adopting conditional conformances (which weren't available when this was originally written), but that turns out to lead to quite the rabbit hole of issues so that'll have to wait.
@designatednerd designatednerd added this to the Next Release milestone Jun 17, 2020
@groue
Copy link
Contributor

groue commented Jul 9, 2021

I got this "Optional is only JSONEncodable if Wrapped is" fatal error in the following scenario:

  1. The build phase generates Swift code with the --passthroughCustomScalars option
  2. One of my custom scalars is a type that implements Apollo.JSONDecodable
  3. Apollo crashes as it is logging a request response 💣

A fix is to also add the JSONEncodable conformance to the custom scalar (so that it can be logged).

Feature request: please have the SDK learn how to log types that do not adopt JSONEncodable. I do not need this functionality in my custom scalar type.

@designatednerd
Copy link
Contributor

@groue Can you clarify something here: Is the custom scalar's Apollo.JSONDecodable implementation something you have added or something provided by the SDK? If it is the latter, what scalar is it?

@groue
Copy link
Contributor

groue commented Jul 9, 2021

Hello @designatednerd. It is a fully custom type: a "base64 data" that consumes a json string from which it base64-decodes its Data property.

EDIT: it looks like:

extension GraphQL {
    public struct Base64: JSONDecodable {
        public let data: Data
        public init(jsonValue value: JSONValue) throws {
            let string = try String(jsonValue: value)
            guard let data = Data(base64Encoded: string) else {
                throw JSONDecodingError.couldNotConvert(value: value, to: Base64.self)
            }
            self.data = data
        }
    }
}

@designatednerd
Copy link
Contributor

Got it - thank you for the clarification!

@AnthonyMDev
Copy link
Contributor

@designatednerd Do we need a new issue to track this?

@designatednerd
Copy link
Contributor

@AnthonyMDev Yeah that's probably a good idea, I'll open one

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
caching crash Issues which cause crashes
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants