-1

I have written this func to save codable objects in my keychain store. I passed a String API key to this func as the value and in 13+ iOS versions it works perfectly fine.

class StoreManager {
     public static func save<T: Codable>(_ value: T, forKey key: String) {
        guard let data = try? JSONEncoder().encode(value) else { 
           return 
        }
        try? keychain.set(data, key: key)
     }
}


let dummyApiKey = "1234567=8+90"
StoreManager().save(dummyApiKey, forKey: "id")

In iOS 12 though, somehow the String value cannot be encoded, and the guard statement goes into return. Does anyone know what's different in iOS 12 that makes it impossible to encode?

limbo
  • 329
  • 1
  • 11
  • 1
    Start by doing a real `do`/`try`/`catch` instead of `try`, it could explain why it fails... – Larme Sep 14 '21 at 09:55
  • The error would be then `Error: invalidValue("1234567=8+90", Swift.EncodingError.Context(codingPath: [], debugDescription: "Top-level String encoded as string JSON fragment.", underlyingError: nil))`. "String" being a Valid JSON needs fragment (that's why there is that parameter in old `JSONSerialization`). Some explaination: https://stackoverflow.com/questions/50257242/jsonencoder-wont-allow-type-encoded-to-primitive-value and workaround, using `JSONSerialization` that can have the `.allowFragment` param. – Larme Sep 14 '21 at 09:58
  • @Larme "The data couldn’t be written because it isn’t in the correct format" thats the error that was caught. – limbo Sep 14 '21 at 10:06
  • You printed `error.localizedDescription` instead of `error`. `error.localizedDescription` is for user, not developer. – Larme Sep 14 '21 at 10:12

1 Answers1

0

If you can't debug yourself, NEVER USE try?. With more experience, I'd say that we tend to not use try?, but sometimes we do. But when we write try?, we are able to find an possible issue, ie debug if needed.

Let's do a proper try then, with a do/catch:

do {
    let data = try JSONEncoder().encode(value)
    print(data)
} catch {
    print("Error: \(error)")
}

And read the output. Also, don't output error.localizedDescription which is more intented for the user, not the developer, output error.

The output is:

Error: invalidValue("1234567=8+90", 
                    Swift.EncodingError.Context(codingPath: [], 
                    debugDescription: "Top-level String encoded as string JSON fragment.", 
                    underlyingError: nil))

In other words: A JSON is 99% of the time a Dictionary or an Array at top level, in Codable wording, it's a Codable struct with its property or an array of it. But, some JSON reference allow a simple String to be JSON valid.
I guess it's been allowed with JSONEncoder in iOS13+ (see related question)
In iOS, it's a fragment. The option is available with older JSONSerialization but not with JSONEncoder.

let data = JSONSerialization.data(withJSONObject: value, options: [.fragmentsAllowed])

Now, you can use @available(iOS 13, *) to call either JSONEncoder or JSONSerialization:

if #available(iOS 13.0, *) {
    //Do the JSONEncoder thing
} else {
    //Do the JSONSerialization Thing
}

Disclaimer:
The start of the answer with the try? is a copy/paste from another of MY OWN answer. So no plagiarism.

Larme
  • 24,190
  • 6
  • 51
  • 81