How should I model a dictionary property on a Realm object so when encoded to JSON I can get this:
{
"firstName": "John",
"lastName": "Doe",
"favoriteThings": {
"car": "Audi R8",
"fruit": "strawberries",
"tree": "Oak"
}
}
I tried creating a new Object FavoriteThings with 'key' and 'value' properties as I've seen elsewhere...
public class Person: Object {
#objc dynamic var firstName = ""
#objc dynamic var lastName = ""
var favoriteThings = List<FavoriteThings>()
}
But List gives me an array, naturally, when I encode it to JSON. I don't want an array. I'm using Swift Codable.
{
"firstName": "John",
"lastName": "Doe",
"favoriteThings": [
{
"key": "fruit",
"value": "strawberries"
},
{
"key": "tree",
"value": "Oak"
}
],
}
Any pointers appreciated!
Gonzalo
As you know, Lists are encoded into json arrays by default. So, to encode a List into a Dictionary you'll have to implement a custom encode method to do exactly that.
public class FavoriteThings: Object {
#objc dynamic var key = ""
#objc dynamic var value = ""
convenience init(key: String, value: String) {
self.init()
self.key = key
self.value = value
}
}
public class Person: Object, Encodable {
enum CodingKeys: String, CodingKey {
case firstName
case lastName
case favoriteThings
}
#objc dynamic var firstName = ""
#objc dynamic var lastName = ""
let favoriteThings = List<FavoriteThings>()
convenience init(firstName: String, lastName: String, favoriteThings: [FavoriteThings]) {
self.init()
self.firstName = firstName
self.lastName = lastName
self.favoriteThings.append(objectsIn: favoriteThings)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(firstName, forKey: .firstName)
try container.encode(lastName, forKey: .lastName)
var favThings: [String: String] = [:]
for thing in favoriteThings {
favThings[thing.key] = thing.value
}
try container.encode(favThings, forKey: .favoriteThings)
}
}
And the usage would be like this:
func testEncode() {
let john = Person(
firstName: "John",
lastName: "Doe",
favoriteThings: [
.init(key: "car", value: "Audi R8"),
.init(key: "fruit", value: "strawberries"),
.init(key: "tree", value: "Oak"),
])
let encoder = JSONEncoder()
let data = try! encoder.encode(john)
if let string = String(data: data, encoding: .utf8) {
print(string)
}
}
Which prints:
{"firstName":"John","favoriteThings":{"car":"Audi R8","fruit":"strawberries","tree":"Oak"},"lastName":"Doe"}
Related
I'm having a problem to pass this Codable as parameter for my api service.
The value key has different types (String, Bool, Int). I always get String values of those whenever I pass this as my parameter :(
{
"deviceId": "aabbcc112233",
"commands": [
{
"code": "mode",
"value": "play"
},
{
"code": "start",
"value": false
},
{
"code": "timer",
"value": 4
}
],
"type": "activity"
}
My current code is this:
struct MyParameter: Codable {
var deviceId: String
var commands: [CommandStatus]
var type: String
enum CodingKeys: String, CodingKey {
case deviceId = "deviceId"
case commands = "commands"
case type = "type"
}
}
struct CommandStatus: Codable {
var code: String
var value: String
init(code: String, value: String) {
self.code = code
self.value = value
}
init(code: String, value: Int) {
self.code = code
self.value = String(value)
}
init(code: String, value: Bool) {
self.code = code
self.value = String(value)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(value, forKey: .value)
try container.encode(value.boolValue, forKey: .value)
try container.encode((value as NSString).integerValue, forKey: .value)
}
enum CodingKeys: String, CodingKey {
case code, value
}
}
Can someone help me understand more on how to do this?
I have a model that looks like:
public struct Profile {
public let bio: String?
public let company: String
public let createdDate: Date
public let department: String
public let email: String
public let firstName: String
public let coverImageURL: URL?
public let jobTitle: String
public let lastName: String
public let location: String?
public let name: String
public let profileImageURL: URL?
public let roles: [String]
public let isActive: Bool
public let updatedDate: Date?
public let userID: String
public init(
bio: String?, company: String, createdDate: Date, department: String, email: String, firstName: String, coverImageURL: URL?, jobTitle: String,
lastName: String, location: String?, name: String, profileImageURL: URL?, roles: [String], isActive: Bool, updatedDate: Date?, userID: String) {
self.bio = bio
self.company = company
self.createdDate = createdDate
self.department = department
self.email = email
self.firstName = firstName
self.coverImageURL = coverImageURL
self.jobTitle = jobTitle
self.lastName = lastName
self.location = location
self.name = name
self.profileImageURL = profileImageURL
self.roles = roles
self.isActive = isActive
self.updatedDate = updatedDate
self.userID = userID
}
}
extension Profile: Equatable { }
I am trying to create a tuple that represents it as (model: Profile, json: [String: Any])
using the following method -
func makeProfile() -> (model: Profile, json: [String: Any]) {
let updatedDate = Date()
let updatedDateStr = updatedDate.toString(dateFormat: "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX")
let createdDate = Date()
let createdDateStr = createdDate.toString(dateFormat: "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX")
let coverImageURL = makeURL("https://headerUri.com")
let profileImageURL = makeURL("https://pictureUri.com")
let model = Profile(
bio: "Some Bio",
company: "Cool Job INC",
createdDate: createdDate,
department: "Engineering",
email: "name#domain.tld",
firstName: "Anne",
coverImageURL: coverImageURL,
jobTitle: "Test Dummy",
lastName: "Employee",
location: "London",
name: "Anne Employee",
profileImageURL: profileImageURL,
roles: ["ADMIN"],
isActive: true,
updatedDate: updatedDate,
userID: UUID().uuidString
)
let json: [String: Any] = [
"bio": model.bio,
"company": model.company,
"createdDate": createdDateStr,
"department": model.department,
"email": model.email,
"firstName": model.firstName,
"headerUri": model.coverImageURL?.absoluteString,
"jobTitle": model.jobTitle,
"lastName": model.lastName,
"location": model.location,
"name": model.name,
"pictureUri": model.profileImageURL?.absoluteString,
"roles": model.roles,
"isActive": model.isActive,
"updatedDate": updatedDateStr,
"userId": model.userID
].compactMapValues { $0 }
return (model: model, json: json)
}
}
extension Date {
func toString(dateFormat format: String) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = format
return dateFormatter.string(from: self)
}
}
All of the optional properties are showing a warning Expression implicitly coerced from 'String?' to 'Any'
I have added .compactMapValues { $0 } to the dict but has had no effect.
How can I clear this warning?
You're getting an error because you trying to implicitly coerce an optional value (which could be nil) to Any, which hides that the value could be nil.
compactMapValues doesn't guarantee at compile-time that the value is not nil; all the compiler knows is that something that is an optional (like String?) is now treated as a non-optional Any.
One way you could avoid the error is by providing a default value instead of the optional:
let json: [String: Any] = [
"bio": model.bio ?? ""
// ...
]
Alternatively, you can create a dictionary of: [String, Any?] to assign values to, and then use .compactMapValues, which would give you back non-optional values of Any.
let withOptionals: [String: Any?] = [
"bio": model.bio,
// ...
]
let json: [String, Any] = withOptionals.compactMapValues { $0 }
You're getting the warning because the values are optional when you initialize the dictionary. Your .compactMapValues { $0 } won't help because by then it's "too late".
Instead you could initialize an empty dictionary and add the values 1 by 1.
var json = [String: Any]()
json["bio"] = model.bio
json["company"] = model.company
// all of the other values
json["userId"] = model.userID
I have:
array = [["name": String, "lastName": String],
["name": String, "lastName": String],
["name": String, "lastName": String]]
(a: [Сlass.[String:String]]) -> [Class.SomeStruct] {}
How to make a structure with its properties from this array?
Like this:
struct SomeStruct {
let name: String
let lastName: String
}
You can use map or compactMap to transform your dictionaries into structs.
let array = [["name": "String", "lastName": "String"],
["name": "String", "lastName": "String"],
["name": "String", "lastName": "String"]]
struct SomeStruct {
let name: String
let lastName: String
}
let values = array.compactMap { data -> SomeStruct? in
guard let name = data["name"], let lastName = data["lastName"] else {
return nil
}
return SomeStruct(name: name, lastName: lastName)
}
Note: The compactMap will silently ignore all dictionaries that doesn't contains a name or a lastName key.
Use Codable
// MARK: - SomeStructElement
struct SomeStructElement: Codable, Equatable {
let name: Int?
let lastName: String?
enum CodingKeys: String, CodingKey {
case name = "name"
case lastName = "lastName"
}
}
And then use JSONDecoder:
let someStructArray: Array<SomeStructElement> = try? JSONDecoder().decode(Array<SomeStructElement>.self, from: data) ?? []
Source: Encoding and Decoding Custom Types (Apple developer)
How does the Swift 4 Decodable protocol cope with a dictionary containing a key whose name is not known until runtime? For example:
[
{
"categoryName": "Trending",
"Trending": [
{
"category": "Trending",
"trailerPrice": "",
"isFavourit": null,
"isWatchlist": null
}
]
},
{
"categoryName": "Comedy",
"Comedy": [
{
"category": "Comedy",
"trailerPrice": "",
"isFavourit": null,
"isWatchlist": null
}
]
}
]
Here we have an array of dictionaries; the first has keys categoryName and Trending, while the second has keys categoryName and Comedy. The value of the categoryName key tells me the name of the second key. How do I express that using Decodable?
The key is in how you define the CodingKeys property. While it's most commonly an enum it can be anything that conforms to the CodingKey protocol. And to make dynamic keys, you can call a static function:
struct Category: Decodable {
struct Detail: Decodable {
var category: String
var trailerPrice: String
var isFavorite: Bool?
var isWatchlist: Bool?
}
var name: String
var detail: Detail
private struct CodingKeys: CodingKey {
var intValue: Int?
var stringValue: String
init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
init?(stringValue: String) { self.stringValue = stringValue }
static let name = CodingKeys.make(key: "categoryName")
static func make(key: String) -> CodingKeys {
return CodingKeys(stringValue: key)!
}
}
init(from coder: Decoder) throws {
let container = try coder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.detail = try container.decode([Detail].self, forKey: .make(key: name)).first!
}
}
Usage:
let jsonData = """
[
{
"categoryName": "Trending",
"Trending": [
{
"category": "Trending",
"trailerPrice": "",
"isFavourite": null,
"isWatchlist": null
}
]
},
{
"categoryName": "Comedy",
"Comedy": [
{
"category": "Comedy",
"trailerPrice": "",
"isFavourite": null,
"isWatchlist": null
}
]
}
]
""".data(using: .utf8)!
let categories = try! JSONDecoder().decode([Category].self, from: jsonData)
(I changed isFavourit in the JSON to isFavourite since I thought it was a mispelling. It's easy enough to adapt the code if that's not the case)
You can write a custom struct that functions as a CodingKeys object, and initialize it with a string such that it extracts the key you specified:
private struct CK : CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
return nil
}
}
Thus, once you know what the desired key is, you can say (in the init(from:) override:
let key = // whatever the key name turns out to be
let con2 = try! decoder.container(keyedBy: CK.self)
self.unknown = try! con2.decode([Inner].self, forKey: CK(stringValue:key)!)
So what I ended up doing is making two containers from the decoder — one using the standard CodingKeys enum to extract the value of the "categoryName" key, and another using the CK struct to extract the value of the key whose name we just learned:
init(from decoder: Decoder) throws {
let con = try! decoder.container(keyedBy: CodingKeys.self)
self.categoryName = try! con.decode(String.self, forKey:.categoryName)
let key = self.categoryName
let con2 = try! decoder.container(keyedBy: CK.self)
self.unknown = try! con2.decode([Inner].self, forKey: CK(stringValue:key)!)
}
Here, then, is my entire Decodable struct:
struct ResponseData : Codable {
let categoryName : String
let unknown : [Inner]
struct Inner : Codable {
let category : String
let trailerPrice : String
let isFavourit : String?
let isWatchList : String?
}
private enum CodingKeys : String, CodingKey {
case categoryName
}
private struct CK : CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
return nil
}
}
init(from decoder: Decoder) throws {
let con = try! decoder.container(keyedBy: CodingKeys.self)
self.categoryName = try! con.decode(String.self, forKey:.categoryName)
let key = self.categoryName
let con2 = try! decoder.container(keyedBy: CK.self)
self.unknown = try! con2.decode([Inner].self, forKey: CK(stringValue:key)!)
}
}
And here's the test bed:
let json = """
[
{
"categoryName": "Trending",
"Trending": [
{
"category": "Trending",
"trailerPrice": "",
"isFavourit": null,
"isWatchlist": null
}
]
},
{
"categoryName": "Comedy",
"Comedy": [
{
"category": "Comedy",
"trailerPrice": "",
"isFavourit": null,
"isWatchlist": null
}
]
}
]
"""
let myjson = try! JSONDecoder().decode(
[ResponseData].self,
from: json.data(using: .utf8)!)
print(myjson)
And here's the output of the print statement, proving that we've populated our structs correctly:
[JustPlaying.ResponseData(
categoryName: "Trending",
unknown: [JustPlaying.ResponseData.Inner(
category: "Trending",
trailerPrice: "",
isFavourit: nil,
isWatchList: nil)]),
JustPlaying.ResponseData(
categoryName: "Comedy",
unknown: [JustPlaying.ResponseData.Inner(
category: "Comedy",
trailerPrice: "",
isFavourit: nil,
isWatchList: nil)])
]
Of course in real life we'd have some error-handling, no doubt!
EDIT Later I realized (in part thanks to CodeDifferent's answer) that I didn't need two containers; I can eliminate the CodingKeys enum, and my CK struct can do all the work! It is a general purpose key-maker:
init(from decoder: Decoder) throws {
let con = try! decoder.container(keyedBy: CK.self)
self.categoryName = try! con.decode(String.self, forKey:CK(stringValue:"categoryName")!)
let key = self.categoryName
self.unknown = try! con.decode([Inner].self, forKey: CK(stringValue:key)!)
}
Here's what I eventually came up for this json:
let json = """
{
"BTC_BCN":{
"last":"0.00000057",
"percentChange":"0.03636363",
"baseVolume":"47.08463318"
},
"BTC_BELA":{
"last":"0.00001281",
"percentChange":"0.07376362",
"baseVolume":"5.46595029"
}
}
""".data(using: .utf8)!
We make such a structure:
struct Pair {
let name: String
let details: Details
struct Details: Codable {
let last, percentChange, baseVolume: String
}
}
then decode:
if let pairsDictionary = try? JSONDecoder().decode([String: Pair.Details].self, from: json) {
var pairs: [Pair] = []
for (name, details) in pairsDictionary {
let pair = Pair(name: name, details: details)
pairs.append(pair)
}
print(pairs)
}
It is also possible to call not pair.details.baseVolume, but pair.baseVolume:
struct Pair {
......
var baseVolume: String { return details.baseVolume }
......
Or write custom init:
struct Pair {
.....
let baseVolume: String
init(name: String, details: Details) {
self.baseVolume = details.baseVolume
......
I am trying figure out how could I pars Realm list using new feature in Swift 4, Decodable protocol.
Here is a example JSON:
[{
"name": "Jack",
"lastName": "Sparrow",
"number": "1",
"address": [
{
"city": "New York",
"street": "av. test"
}
]
},
{
"name": "Cody",
"lastName": "Black",
"number": "2"
},
{
"name": "Name",
"lastName": "LastName",
"number": "4",
"address": [
{
"city": "Berlin",
"street": "av. test2"
},
{
"city": "Minsk",
"street": "av. test3"
}
]
}]
And Realm Models:
Person
public final class Person: Object, Decodable {
#objc dynamic var name = ""
#objc dynamic var lastName = ""
var address = List<Place>()
override public static func primaryKey() -> String? {
return "lastName"
}
private enum CodingKeys: String, CodingKey { case name, lastName, address}
convenience public init(from decoder: Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.lastName = try container.decode(String.self, forKey: .lastName)
self.address = try container.decodeIfPresent(List<Place>.self, forKey: .address) ?? List()
}
}
Place
public final class Place: Object, Decodable {
#objc dynamic var city = ""
#objc dynamic var street = 0
override public static func primaryKey() -> String? {
return "street"
}
// We dont need to implement coding keys becouse there is nothing optional and the model is not expanded by extra properties.
}
And the result of parsing this JSON would be:
[Person {
name = Jack;
lastName = Sparrow;
number = 1;
address = List<Place> <0x6080002496c0> (
);
}, Person {
name = Cody;
lastName = Black;
number = 2;
address = List<Place> <0x6080002496c0> (
);
}, Person {
name = Name;
lastName = LastName;
number = 4;
address = List<Place> <0x6080002496c0> (
);
As we can see our list are always empty.
self.address = try container.decodeIfPresent(List<Place>.self, forKey: .address) ?? List()
will always be a nil.
Also I am extending List by :
extension List: Decodable {
public convenience init(from decoder: Decoder) throws {
self.init()
}
}
Any ideas what might be wrong ?
EDIT
struct LoginJSON: Decodable {
let token: String
let firstCustomArrayOfObjects: [FirstCustomArrayOfObjects]
let secondCustomArrayOfObjects: [SecondCustomArrayOfObjects]
let preferences: Preferences
let person: [Person]
}
Each property (instead of token) is a type of Realm Object and the last one is the one from above.
Thanks!
You cannot go directly from your JSON to a List. What's in the JSON is an array. So this line won't work:
self.address = try container.decodeIfPresent(List<Place>.self, forKey: .address) ?? List()
You have to start by fetching the array:
if let arr = try container.decodeIfPresent(Array<Place>.self, forKey: .address) {
// arr is now an array of Place
self.address = // make a List from `arr`, however one does that
} else {
self.address = nil
}