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 #1536

Closed
ruizmarc opened this issue Nov 24, 2020 · 12 comments
Closed

Fatal error: Optional is only JSONEncodable if Wrapped #1536

ruizmarc opened this issue Nov 24, 2020 · 12 comments

Comments

@ruizmarc
Copy link

Bug report

Everything was working fine in 0.36 but after updating to 0.37 this error message appears:

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

Versions

Please fill in the versions you're currently using:

  • apollo-ios SDK version: 0.37
  • Xcode version: 12.2
  • Swift version: 5.3
  • Package manager: Swift Package Manager

Steps to reproduce

Schema:

type Query {
  translations(language: String!, platforms: [TranslationPlatform]!, version: String): JSON
}

Typealias for JSON

public typealias JSON = [String: Any?]

Query:

query getTranslations($language: String!, $platforms: [TranslationPlatform]!, $version: String) {
      translations(language: $language, platforms: $platforms, version: $version)
  }

Response Handling:

ApolloService.shared.apollo.fetch(query: translations, cachePolicy: .fetchIgnoringCacheData) { (result) in
        switch result {
        case .success(let result):
          if result.errors == nil {
            guard let dictionary = result.data?.jsonObject["translations"] as? [String: String] else {
              return callback()
            }
      ....
   }
}

Response format

{
    "data": {
        "translations": {
            "CANCEL": "Cancel",
            "CLOSE": "Close",
            "CREATE": "Create"
            ....
       }
}

It crashes when executing jsonObject. With previous versions, everything was working fine.

@designatednerd
Copy link
Contributor

Can I see the code you're using to make JSON conform to the JSONDecodable protocol?

@ruizmarc
Copy link
Author

Hi,

Since 0.30.0 we stopped using a custom function to map our JSON to a JSONDecadable/JSONEncodable, as for this changelog note.

POSSIBLY BREAKING: Works around an issue that could cause some attempts to store untyped JSON dictionaries to throw unexpected errors about optional encoding. This also added handling of creating a dictionary from a JSONValue, which may cause problems if you've already implemented this yourself, but which should mostly just replace the need to implement it yourself. Please file issues ASAP if you run into problems here. (#1317)

It has been working fine as it is since 0.30.0 to 0.36.0. It has broken since we upgraded to 0.37.0

@designatednerd
Copy link
Contributor

Hmmm. I'll have to look into that, that's odd.

@designatednerd
Copy link
Contributor

@ruizmarc Can you share the full dictionary you're getting back so I can use it to do some testing please?

@ruizmarc
Copy link
Author

Here you have the payload of the request: https://pastebin.com/eTbhsheb

Thanks for checking it!

@designatednerd
Copy link
Contributor

So I pulled the payload in and the JSON was failing to parse until I found all the \" instances and changed them to \\". These existed in a couple of the strings around terms and conditions (gotta love legalese!). Once I did that, the following sample query, which assumes the custom JSON scalar type is named CustomJSON but which I believe should otherwise mirror your query's results, parses without any problem:

func testProvidedTranslations() throws {
  typealias CustomJSON = [String: Any?]
  class GetTranslationsQuery: GraphQLQuery {
    var operationDefinition: String {
      """
query GetTranslations {
  translations
}
"""
      }
      
    var operationName: String = "GetTanslations"
      
    struct Data: GraphQLSelectionSet {
      static var selections: [GraphQLSelection] = [
        GraphQLField("translations", type: .nonNull(.scalar(CustomJSON.self))),
      ]
        
      var resultMap: ResultMap
        
      init(unsafeResultMap: ResultMap) {
        resultMap = unsafeResultMap
      }
        
      var translations: CustomJSON {
        get {
          return resultMap["translations"]! as! CustomJSON
        }
        set {
          resultMap.updateValue(newValue, forKey: "translations")
        }
      }
    }
  }
    
  let string = """
[ raw string from pastebin ]
"""
    
  let jsonData = try XCTUnwrap(string.data(using: .utf8))
  let parsedJSON = try JSONSerializationFormat.deserialize(data: jsonData)
  let typedJSON = try XCTUnwrap(parsedJSON as? JSONObject)
  
  let response = GraphQLResponse(operation: SomeQuery(), body: typedJSON)
    
  let result = try response.parseResultFast()
    
  XCTAssertNotNil(result.data)
  XCTAssertEqual(result.data?.translations["INITIAL_CHARGE_LEVEL"] as? String, "Initial charge level")
}

I'm betting what's happening is the underlying error is getting swallowed by the parser somewhere. Would you mind trying to update the escape sequences for quotation marks on your end to see if that might be the issue?

@ruizmarc
Copy link
Author

ruizmarc commented Nov 25, 2020

Hi!

Thanks for your investigation, unfortunately, it didn't worked taking out the quotes. However, looking at your example, I changed the code from:

guard let dictionary = result.data?.jsonObject["translations"] as? [String: String] else {
              return callback()
}

to the following one, and it worked even without removing the quotes:

guard let dictionary = result.data?.resultMap["translations"] as? [String: String] else {
              return callback()
}

So at this moment the app is working again with 0.37.0 with just changing this. I don't know if it is because it was not intended that we use jsonObject or it is really an issue. I leave up to you to close the issue or not depending on this.

In any case, thanks for your attention.

@designatednerd
Copy link
Contributor

What about using result.data?.translations? JSONObject is the name of the type, not the property, in my example.

If you can share the generated code for your query I can get you something more accurate.

@ruizmarc
Copy link
Author

Hi,

It also works with your suggestion:

guard let dictionary = result.data?.translations as? [String: String] else {
              return callback()
}

What I was using is this: https://developer.apple.com/documentation/foundation/jsonserialization/1415493-jsonobject

But it is clear it is not necessary to use it anymore. We have been using apollo-ios since it very beginning and at some point in the past, that was the only way we could get it working, but with the pass of the time it has gotten much simpler to work with custom types like JSON, using jsonObject it's like legacy code, I don't think I should expect it to work.

Here you have the generated code for the query with version 0.37.0:

public final class GetTranslationsQuery: GraphQLQuery {
  /// The raw GraphQL definition of this operation.
  public let operationDefinition: String =
    """
    query getTranslations($language: String!, $platforms: [TranslationPlatform]!, $version: String) {
      translations(language: $language, platforms: $platforms, version: $version)
    }
    """

  public let operationName: String = "getTranslations"

  public var language: String
  public var platforms: [TranslationPlatform?]
  public var version: String?

  public init(language: String, platforms: [TranslationPlatform?], version: String? = nil) {
    self.language = language
    self.platforms = platforms
    self.version = version
  }

  public var variables: GraphQLMap? {
    return ["language": language, "platforms": platforms, "version": version]
  }

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

    public static var selections: [GraphQLSelection] {
      return [
        GraphQLField("translations", arguments: ["language": GraphQLVariable("language"), "platforms": GraphQLVariable("platforms"), "version": GraphQLVariable("version")], type: .scalar(JSON.self)),
      ]
    }

    public private(set) var resultMap: ResultMap

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

    public init(translations: JSON? = nil) {
      self.init(unsafeResultMap: ["__typename": "Query", "translations": translations])
    }

    public var translations: JSON? {
      get {
        return resultMap["translations"] as? JSON
      }
      set {
        resultMap.updateValue(newValue, forKey: "translations")
      }
    }
  }
}

@designatednerd
Copy link
Contributor

OK - I'm still not totally clear on where jsonObject is coming from as a property on Data, but maybe it was in an older version of the SDK.

In any case, it seems like we've gotten to the bottom of this - do you mind if we close this out?

@TizianoCoroneo
Copy link
Contributor

jsonObject is in an extension of GraphQLSelectionSet, which Data conforms to. It just calls resultMap.jsonObject

@ruizmarc
Copy link
Author

OK - I'm still not totally clear on where jsonObject is coming from as a property on Data, but maybe it was in an older version of the SDK.

In any case, it seems like we've gotten to the bottom of this - do you mind if we close this out?

On my side, we can close the issue. Thanks for your time and kind attention.

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