@Persisted property wrapper and codable

I am having an issue with the new @Persisted property wrapper and decoding JSON. If I have an optional variable and it is present in the JSON, then the decoding is successful, but if I have an optional variable and that value is missing from the JSON, then the decoding fails. This is only an issue using the @Persisted property wrapper and everything works as expected using the old method of @objc and dynamic vars.

This works,

@objcMembers
public class MyObject: Object, ObjectKeyIdentifiable, Codable {
    public private(set) dynamic var id: String = ""
    public private(set) dynamic var myVar1: String
    public private(set) dynamic var myVar2: String?
    
    public override class func primaryKey() -> String? {
        "id"
    }
}

This doesn’t,

public class MyObject: Object, ObjectKeyIdentifiable, Codable {
    @Persisted(primaryKey: true) public private(set) var id: String = ""
    @Persisted public private(set) var myVar1: String
    @Persisted public private(set) var myVar2: String?
}

Hey @Thomas_Rademaker , welcome to the community.

What version are you using? This was a bug in 10.10 that was fixed in 10.11.

I am using 10.11 via Swift Package Manager

Hmm… how does the decoding fail? Is there an error?

This is the error,

“No value associated with key CodingKeys(stringValue: “myVar2”, intValue: nil) (“myVar2”), converted to my_var2.”, underlyingError: nil))

And my decoder is set to convert from snake case,

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase

Hey @Jason_Flax I put together a small sample app to help the team debug. This project is pulling in realmDatabase v 11.1.1 and realm v 10.11.0 via swift package manager

testApp.swift

import SwiftUI

@main
struct testApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

ContentView the main SwiftUI view

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .padding()
            .onAppear {
                API().run {
                    switch $0 {
                    case .success(let nflPlayers):
                        print("NFLPlayers: \(String(describing: nflPlayers))")
                    case .failure(let error):
                        print("ERROR: \(error)")
                    }
                }
            }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

NFLPlayer as you can see 2 of the 3 codable objects are not Object. I don’t know if that is contributing to the bug? Both versions of NFLPlayer are here (1 is commented out)

import RealmSwift

//public class NFLPlayer: Object, ObjectKeyIdentifiable, Codable {
//    @Persisted(primaryKey: true) public private(set) var id: String = ""
//    @Persisted public private(set) var draftYear: String?
//    @Persisted public private(set) var draftRound: String?
//}

@objcMembers
public class NFLPlayer: Object, ObjectKeyIdentifiable, Codable {
    public private(set) dynamic var id: String = ""
    public private(set) dynamic var draftYear: String?
    public private(set) dynamic var draftRound: String?

    public override class func primaryKey() -> String? {
        "id"
    }
}

struct NFLPlayers: Codable {
    let timestamp: String?
    let player: [NFLPlayer]?
    let id: String?
}

struct NFLPlayersWrapper: Codable {
    let players: NFLPlayers?
}

This is where the network request is made

import Foundation

enum APIError: Error {
    case general
}

class API {
    func run(completion: @escaping (Result<[NFLPlayer]?, Error>) -> Void) {
        let url = URL(string: "https://api.myfantasyleague.com/2020/export?TYPE=players&DETAILS=1&JSON=1")!
        let task = URLSession.shared.dataTask(with: url, completionHandler: { data, response, error in
            self.handleResponse(data: data, response: response, error: error, completion: completion)
        })
        
        task.resume()
    }
    
    private func handleResponse(data: Data?, response: URLResponse?, error: Error?, completion: @escaping (Result<[NFLPlayer]?, Error>) -> Void) {
        guard let httpResponse = response as? HTTPURLResponse,
              let data = data else {
            completion(.failure(APIError.general))
            return
        }
        switch httpResponse.statusCode {
        case 200...299:
            do {
                let decoder = JSONDecoder()
                decoder.keyDecodingStrategy = .convertFromSnakeCase
                let nflPlayersWrapper = try decoder.decode(NFLPlayersWrapper.self, from: data)
                completion(.success(nflPlayersWrapper.players?.player))
            } catch {
                completion(.failure(error))
            }
        default:
            return
        }
    }
}
1 Like

I am following along and don’t have an answer but I am curious why you’re doing this

When there are no setters or getters within the object - e.g. why private(set)?

Hi @Thomas_Rademaker, we already have a PR in progress that solves this issue, you can follow along it here Fix decoding a `@Persisted` property throws a `DecodingError.keyNotFound` when key is missing by dianaafanador3 · Pull Request #7365 · realm/realm-cocoa · GitHub

1 Like

Thank you @Diana_Maria_Perez_Af . That is exactly what I need.

This topic was automatically closed 5 days after the last reply. New replies are no longer allowed.