Tarvo Mäesepp
Tarvo Mäesepp

Reputation: 4533

How to encode Realm's List<> type

I am trying to encode my Realm database to JSON. Everything is working except the List<> encoding. So my question is, how would you encode List<>? Because the List doesn't conform to Encodable neighter Decodable protocol.

Right now I am doing this:

@objcMembers class User: Object, Codable{
    dynamic var name: String = ""
    let dogs = List<Dog>()


    private enum UserCodingKeys: String, CodingKey {
        case name
        case dogs
    }

    convenience init(name: String) {
        self.init()
        self.name = name
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: UserCodingKeys.self)
        try container.encode(name, forKey: .name)

    }



@objcMembers class Dog: Object, Codable{
    dynamic var name: String = ""
    dynamic var user: User? = nil

    private enum DogCodingKeys: String, CodingKey {
        case name
    }

    convenience init(name: String) {
        self.init()
        name = name
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: DogCodingKeys.self)
        try container.encode(name, forKey: .name)

    }
}

and like this I am trying to do it:

var json: Any?
let user = RealmService.shared.getUsers()
var usersArray = [User]()
for user in users{
     usersArray.append(user)
}

let jsonEncoder = JSONEncoder()
let jsonDecoder = JSONDecoder()
let encodedJson = try? jsonEncoder.encode(portfoliosArray)


        if let data = encodedJson {
            json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments)
            if let json = json {
                print(String(describing: json))
            }
        }

So the question is how I am able to encode the List<Dog>?

Upvotes: 3

Views: 1930

Answers (2)

Cristik
Cristik

Reputation: 32789

You could resort to a mini-hack by making List conform to Encodable:

extension List: Encodable {
    public func encode(to coder: Encoder) throws {
        // by default List is not encodable, throw an exception
        throw NSError(domain: "SomeDomain", code: -1, userInfo: nil)
    }
}

// let's ask it to nicely encode when Element is Encodable
extension List where Element: Encodable {
    public func encode(to coder: Encoder) throws {
        var container = coder.unkeyedContainer()
        try container.encode(contentsOf: self)
    }
}

Two extensions are needed as you can't add protocol conformance and where clauses at the same time.

Also note that this approach doesn't provide compile-time checks - e.g. a List<Cat> will throw an exception an runtime if Cat is not encodable, instead of a nice compile time error.

The upside is lot of boilerplate code no longer needed:

@objcMembers class User: Object, Encodable {
    dynamic var name: String = ""
    let dogs = List<Dog>()

    convenience init(name: String) {
        self.init()
        self.name = name
    }
}

@objcMembers class Dog: Object, Encodable {
    dynamic var name: String = ""
    dynamic var user: User? = nil

    convenience init(name: String) {
        self.init()
        name = name
    }
}

This is also scalable, as adding new classes don't require any encoding code, but with the mentioned downside of not being fully type safe at compile time.

Upvotes: 0

David Pasztor
David Pasztor

Reputation: 54706

To make a Realm object model class with a property of type List conform to Encodable, you can simply convert the List to an Array in the encode(to:) method, which can be encoded automatically.

extension User: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(self.username, forKey: .username)
        let dogsArray = Array(self.dogs)
        try container.encode(dogsArray, forKey: .dogs)
    }
}

Test classes I used (slightly different from the ones in your question, but I already had these on hand and the methods in question will be almost identical regardless of the variable names):

class Dog: Object,Codable {
    @objc dynamic var id:Int = 0
    @objc dynamic var name:String = ""
}

class User: Object, Decodable {
    @objc dynamic var id:Int = 0
    @objc dynamic var username:String = ""
    @objc dynamic var email:String = ""
    let dogs = List<Dog>()

    private enum CodingKeys: String, CodingKey {
        case id, username, email, dogs
    }

    required convenience init(from decoder: Decoder) throws {
        self.init()
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(Int.self, forKey: .id)
        username = try container.decode(String.self, forKey: .username)
        email = try container.decode(String.self, forKey: .email)
        let dogsArray = try container.decode([Dog].self, forKey: .dogs)
        dogs.append(objectsIn: dogsArray)
    }
}

Test the encoding/decoding:

let userJSON = """
{
    "id":1,
    "username":"John",
    "email":"[email protected]",
    "dogs":[
        {"id":2,"name":"King"},
        {"id":3,"name":"Kong"}
    ]
}
"""

do {
    let decodedUser = try JSONDecoder().decode(User.self, from: userJSON.data(using: .utf8)!)
    let encodedUser = try JSONEncoder().encode(decodedUser)
    print(String(data: encodedUser, encoding: .utf8)!)
} catch {
    print(error)
}

Output:

{"username":"John","dogs":[{"id":2,"name":"King"},{"id":3,"name":"Kong"}]}

Upvotes: 7

Related Questions