Apollo-ios: Fatal error ("Optional is only JSONEncodable if Wrapped is") when trying to update SqlNormalizedCache manually after a successful mutation operation

Created on 8 Jul 2020  Â·  19Comments  Â·  Source: apollographql/apollo-ios

While using Apollo iOS 0.29.1, I appear to be running into an issue similar to the one reported here. I asked for help with this issue in Spectrum, and @designatednerd asked me to create a new issue.

I'm trying to update the SqlNormalizedCache (which is setup and working properly) using the result of a successful mutation operation:

apollo.perform(mutation: InsertTaskResponseMutation(taskId: 1, response: "")) { result in
    guard
        let resultData = try? result.get().data,
        let taskResponseDetails = resultData.insertTaskResponses?.returning.first?.fragments.taskResponseDetails
    else {
        return
    }

    apollo.store.withinReadWriteTransaction({ transaction in
        try! transaction.write(object: taskResponseDetails, withKey: "task_responses-\(taskResponseDetails.id)")
    })
}

This is throwing a fatal error in "JSONStandardTypeConversions.swift" at line 109:

Screen Shot 2020-07-08 at 12 01 31 PM

Printing the description of self:

Printing description of self:
â–¿ Optional<Any>
  - some : 07/08/2020

In this case, self is a property on the response property, which is a custom jsonb type. Here's the type alias:

public typealias jsonb = [String : Any?]

extension Dictionary: JSONDecodable {
    public init(jsonValue value: JSONValue) throws {
        guard let dictionary = value as? Dictionary else {
            throw JSONDecodingError.couldNotConvert(value: value, to: Dictionary.self)
        }

        self = dictionary
    }
}

And here's the query:

query ChallengeTaskResponse($taskResponseId: Int) {
    challenge_responses(where: {id: {_eq: $taskResponseId}}) {
        ...ChallengeTaskResponseDetails
    }
}

The mutation:

mutation InsertTaskResponse($response: jsonb) {
    insert_task_responses(objects: {response: $response}) {
        returning {
            ...TaskResponseDetails
        }
    }
}

And, finally, the fragment that's used by both the query and the mutation:

fragment TaskResponseDetails on task_responses {
    id
    response // The 'jsonb' custom data type
}
caching

Most helpful comment

Yep, that fixes the issue!!!

All 19 comments

The problem definitely seems to be in the fact that the value type in the dictionary is Optional<Any>, and that's what it's trying to use to create a key.

Does it work to typealias it to [String: JSONDecodable?] instead?

I'll test that here shortly and report back.

@nateirwin Were you ever able to get this tested out?

@designatednerd: I started to dive into this, ran into a minor issue (that's project-specific and I just need to push through), then got pulled into another issue altogether. This is still critical for our project and it's next on my list, so I'll get to it soon.

I'm happy to close this and re-open when I've had the chance to test, if that's helpful.

Nah, leave it open for now, I'll keep annoying you about it from time to time 😇

Ok, reporting back :-)

Switching the typealias to public typealias jsonb = [String : JSONDecodable?] throws an error when loading a query that has a jsonb property on it:

Printing description of error:
â–¿ GraphQLResultError
  â–¿ path : communities.0.organization_community_memberships.1.organization.paper_maps.0.paper_map.status
    â–¿ head : Optional<Node>
      â–¿ some : <Node: 0x11098f010>
  â–¿ underlying : JSONDecodingError
    â–¿ couldNotConvert : 2 elements
      â–¿ value : 24 elements
        â–¿ 0 : 2 elements
          - key : status
          - value : Uploading tiles complete
        â–¿ 1 : 2 elements
          - key : min_zoom
          - value : 9
        â–¿ 2 : 2 elements
          - key : extent_max_lon
          - value : -100.1
        â–¿ 3 : 2 elements
          - key : extent_min_lat
          - value : 37.1
        â–¿ 4 : 2 elements
          - key : tileset
          - value : tileset_548
        â–¿ 5 : 2 elements
          - key : pdfPath
          - value : https://test.xyz/test.pdf
        â–¿ 6 : 2 elements
          - key : updated_at
          - value : 2019-07-29T15:04:09.623Z
        â–¿ 7 : 2 elements
          - key : extent_max_lat
          - value : 34.1
        â–¿ 8 : 2 elements
          - key : localTilesPath
          - value : https://test.xyz/tiles/
        â–¿ 9 : 2 elements
          - key : geotiff_path
          - value : /image_georef.tif
        â–¿ 10 : 2 elements
          - key : geotiff_3857_path
          - value : /image_georef_3857.tif
        â–¿ 11 : 2 elements
          - key : name
          - value : Name Here
        â–¿ 12 : 2 elements
          - key : max_zoom
          - value : 15
        â–¿ 13 : 2 elements
          - key : id
          - value : 548
        â–¿ 14 : 2 elements
          - key : temp_georefTilesPath
          - value : <null>
            - super : NSObject
        â–¿ 15 : 2 elements
          - key : ground_control_points
          - value : [[1071.640625,2925.1640625,-122.104733996093,37.714753539402],[1355.44921875,2053.29296875,-122.09752430208,37.7391649876836],[1192.0390625,1299.8125,-122.105021746829,37.7592430319794],[669.765625,594.1796875,-122.124813348055,37.7770496273366]]
        â–¿ 16 : 2 elements
          - key : png_path
          - value : /final.png
        â–¿ 17 : 2 elements
          - key : page_number
          - value : <null> { ... }
        â–¿ 18 : 2 elements
          - key : image_width
          - value : 2550
        â–¿ 19 : 2 elements
          - key : created_at
          - value : 2019-07-29T14:57:07.796Z
        â–¿ 20 : 2 elements
          - key : georefTilesPath
          - value : https://test.xyz/tiles_georef
        â–¿ 21 : 2 elements
          - key : image_height
          - value : 3300
        â–¿ 22 : 2 elements
          - key : extent_min_lon
          - value : -115.1
        â–¿ 23 : 2 elements
          - key : file_type
          - value : pdf
      - to : Swift.Dictionary<Swift.String, Swift.Optional<Apollo.JSONDecodable>>

I'm guessing maybe it's bonking on the null values? Honestly, we've run into so many issues with this custom jsonb scalar type that I'm considering trying to back out of using it altogether.

Hm, shouldn't be bonking on nulls, we've got something that theoretically should be handling that. From the underlying error it looks like it's freaking out about the status key which is just a String.

I think a bigger question this brings up is why this data needs to be returned as an arbitrary JSON blob in the first place - why isn't this data returned as something typed?

Well, status is the name of the jsonb property, which (I think) should be of type [String: JSONDecodable?] because of the type alias:

public typealias jsonb = [String : JSONDecodable?]

extension Dictionary: JSONDecodable {
    public init(jsonValue value: JSONValue) throws {
        guard let dictionary = value as? Dictionary else {
            throw JSONDecodingError.couldNotConvert(value: value, to: Dictionary.self)
        }

        self = dictionary
    }
}

That's why I'm scratching my head about this. Maybe my next step is setting some more breakpoints in JSONStandardTypeConversions.swift?

Looking at the JSON, status seems to be a key and value of the dictionary which isn't getting deserialized - can you throw in a breakpoint and print out the raw JSON string (or use a proxying tool to see it) that's coming through? That might help.

Sure, here you go (sorry about the delay):

{
  "data": {
    "communities": [
      {
        "organization_community_memberships": [
          {
            "id": 467,
            "organization": {
              "paper_maps": [
                {
                  "id": 557,
                  "paper_map": {
                    "description": "null",
                    "id": 322,
                    "name": "Name Here",
                    "status": {
                      "id": 548,
                      "name": "Name Here",
                      "status": "Uploading tiles complete",
                      "pdfPath": "https://test.xyz/test.pdf",
                      "max_zoom": 15,
                      "min_zoom": 9,
                      "png_path": "/final.png",
                      "file_type": "pdf",
                      "created_at": "2019-07-29T14:57:07.796Z",
                      "updated_at": "2019-07-29T15:04:09.623Z",
                      "image_width": 2550,
                      "page_number": null,
                      "geotiff_path": "/image_georef.tif",
                      "image_height": 3300,
                      "extent_max_lat": "34.1",
                      "extent_max_lon": "-100.1",
                      "extent_min_lat": "37.1",
                      "extent_min_lon": "-115.1",
                      "localTilesPath": "https://test.xyz/tiles/",
                      "mapbox_tileset": "trailheadlabs.paper_map_548",
                      "georefTilesPath": "https://test.xyz/tiles_georef",
                      "geotiff_3857_path": "/image_georef_3857.tif",
                      "temp_georefTilesPath": null,
                      "ground_control_points": "[[1071.640625,2925.1640625,-122.104733996093,37.714753539402],[1355.44921875,2053.29296875,-122.09752430208,37.7391649876836],[1192.0390625,1299.8125,-122.105021746829,37.7592430319794],[669.765625,594.1796875,-122.124813348055,37.7770496273366]]"
                    }
                  }
                }
              ]
            }
          }
        ]
      }
    ]
  }
}

Ah, ok, status is _also_ the name of the property of the dictionary, not just one of the things in the dictionary.

One thing it didn't occur to me to ask: Is your Dictionary initializer getting hit at all? I wonder if it may need to be constrained to the key and value types you're using

It is getting hit, and it's throwing when that same status property that holds the dictionary.

Here's a thought - have you got an extension that implements JSONEncodable? It looks like you're only implementing JSONDecodable above

Hmm, this is what I get when I start to implement JSONEncodable:

Screen Shot 2020-07-16 at 8 35 13 PM

Aha, that is indeed implemented in the library.

OK, I'll mess around with this some to try and figure out what's going on here.

Thanks for your help!

@nateirwin please try pulling the branch #1317 is opened from and seeing if that fixes your issue. You can put the typealias back to [String: Any?].

Yep, that fixes the issue!!!

This has shipped with 0.30.0 - ready to go for SPM and Carthage, in the process of pushing to trunk on Cocoapods. 🎉

Was this page helpful?
0 / 5 - 0 ratings

Related issues

maxsz picture maxsz  Â·  4Comments

MrAlek picture MrAlek  Â·  3Comments

designatednerd picture designatednerd  Â·  3Comments

hiteshborse12 picture hiteshborse12  Â·  4Comments

marcoreni picture marcoreni  Â·  3Comments