Encode dictionary without adding the coding key enum in Swift - swift

I want to encode a JSON that could be
{"hw1":{"get_trouble":true},"seq":2,"session_id":1}
or
{"hw2":{"get_trouble":true},"seq":3,"session_id":2}
the class for encoding looks like the following
class Request: Codable {
let sessionId, seq:Int
let content:[String:Content]
enum CodingKeys:String, CodingKey{
case sessionId = "session_id"
case seq
case content
}
init(sessionId:Int, seq:Int, content:[String:Content]) {
self.sessionId = sessionId
self.seq = seq
self.content = content
}
}
class Content:Codable{
let getTrouble = true
enum CodingKeys:String, CodingKey {
case getTrouble = "get_trouble"
}
}
how can I encode the request so that I can get the desired result? Currently, if I do
let request = Request(sessionId: session, seq: seq, content: [type:content])
let jsonData = try! encoder.encode(request)
I get
{"content":{"hw1":{"get_trouble":true}},"seq":2,"session_id":1}
and I don't want "content" inside the JSON. Already looked into
Swift Codable: encode structure with dynamic keys
and couldn't figure out how to apply in my use case

As with almost all custom encoding problems, the tool you need is AnyStringKey (it frustrates me that this isn't in stdlib):
struct AnyStringKey: CodingKey, Hashable, ExpressibleByStringLiteral {
var stringValue: String
init(stringValue: String) { self.stringValue = stringValue }
init(_ stringValue: String) { self.init(stringValue: stringValue) }
var intValue: Int?
init?(intValue: Int) { return nil }
init(stringLiteral value: String) { self.init(value) }
}
This just lets you encode and encode arbitrary keys. With this, the encoder is straightforward:
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: AnyStringKey.self)
for (key, value) in content {
try container.encode(value, forKey: AnyStringKey(key))
}
try container.encode(sessionId, forKey: AnyStringKey("session_id"))
try container.encode(seq, forKey: AnyStringKey("seq"))
}
This assumes you mean to allow multiple key/value pairs in Content. I expect you don't; you're just using a dictionary because you want a better way to encode. If Content has a single key, then you can rewrite it a bit more naturally this way:
// Content only encodes getTrouble; it doesn't encode key
struct Content:Codable{
let key: String
let getTrouble: Bool
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(["get_trouble": getTrouble])
}
}
struct Request: Codable {
// ...
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: AnyStringKey.self)
try container.encode(content, forKey: AnyStringKey(content.key))
try container.encode(sessionId, forKey: AnyStringKey("session_id"))
try container.encode(seq, forKey: AnyStringKey("seq"))
}
}
Now that may still bother you because it pushes part of the Content encoding logic into Request. (OK, maybe it just bothers me.) If you put aside Codable for a moment, you can fix that too.
// Encode Content directly into container
extension KeyedEncodingContainer where K == AnyStringKey {
mutating func encode(_ value: Content) throws {
try encode(["get_trouble": value.getTrouble], forKey: AnyStringKey(value.key))
}
}
struct Request: Codable {
// ...
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: AnyStringKey.self)
// And encode into the container (note no "forKey")
try container.encode(content)
try container.encode(sessionId, forKey: AnyStringKey("session_id"))
try container.encode(seq, forKey: AnyStringKey("seq"))
}
}

Related

Swift encoding: multiple .encode strategeis?

I have a model TestModel that encodes data to JSON to send to an API. It looks like this:
// Called by: JSONEncoder().encode(testModelObject)
class TestModel {
enum CodingKeys: String, CodingKey {
case someKey = "some_key"
case otherKey = "other_key"
case thirdKey = "third_key"
case apiID = "id"
// ... lots of other keys
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(someKeyValue, forKey: .someKey)
try container.encode(otherKeyValue, forKey: .otherKey)
try container.encode(thirdKeyValue, forKey: .thirdKey)
// ... lots of other encoded fields
}
}
The above works fine, however sometimes I wish to send a request to a different endpoint that updates just a single attribute. The update is always going to be for the same attribute. At present I'm sending through all data in encode(), which equals a lot of wasted bandwidth.
I'm sure there's an easy way to do this, but docs/google/stackoverflow aren't proving helpful. So: any thoughts on how to create a second encoding strategy along the lines of the below and call it?
func encodeForUpdate(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(apiID, forKey: .apiID)
try container.encode(justOneValueToUpdate, forKey: .someKey)
}
You need to have a single encode(to encoder: Encoder) function but you can solve this by using a specific CodingKey enum for the second strategy
enum SimpleCodingKeys: String, CodingKey {
case thirdKey = "third_key"
case apiID = "id"
}
and then use the userInfo property of JSONEncoder to tell when you want to use this second enum. First we need a key to use
extension TestModel {
static var useSimpleCodingKeys: CodingUserInfoKey {
return CodingUserInfoKey(rawValue: "useSimpleCodingKeys")!
}
}
and then adjust the encoding function
func encode(to encoder: Encoder) throws {
let useSimple = encoder.userInfo[Self.useSimpleCodingKeys] as? Bool ?? false
if useSimple {
var container = encoder.container(keyedBy: SimpleCodingKeys.self)
try container.encode(apiID, forKey: .apiID)
try container.encode(thirdKeyValue, forKey: .thirdKey)
} else {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(someKeyValue, forKey: .someKey)
try container.encode(otherKeyValue, forKey: .otherKey)
try container.encode(thirdKeyValue, forKey: .thirdKey)
...
}
}
And of course set this value in the dictionary when encoding
let encoder = JSONEncoder()
encoder.userInfo[TestModel.useSimpleCodingKeys] = true

how do you set nil to codable enum when unexpected value in swift iOS

I'm using Codable for my WebRequest response which is returning some predefined string or number. So, I'm using Enums for those. But when some unexpected value arrive to Response at that my Codable fails to decode.
Here some code for better understanding.
class WebUser: Codable, Equatable {
static func == (lhs: WebUser, rhs: WebUser) -> Bool {
return lhs.id == rhs.id
}
...
var mobileNumberPrivacy: CommonPrivacyOption?
var emailPrivacy: CommonPrivacyOption?
var dobPrivacy: CommonPrivacyOption?
...
}
enum CommonPrivacyOption: Int, CaseIterable, Codable {
case privacyOnlyMe = 1, privacyPublic, privacyFriends, privacyFriendsOfFriends
//Does not help this optional init function
/*init?(rawValue: Int) {
switch rawValue {
case 1: self = .privacyOnlyMe
case 2: self = .privacyPublic
case 3: self = .privacyFriends
case 4: self = .privacyFriendsOfFriends
default: return nil
}
}*/
}
but sometimes from WebServer I'm getting, 0 for dobPrivacy at that time I'm getting DecodingError.dataCorrupted exception with context Cannot initialize CommonPrivacyOption from invalid Int value 0
As I expect to dobPrivacy nil when I get other values then 1/2/3/4.
EDIT:
let dict1 = [
"id": 2,
"mobileNumberPrivacy": 3,
"emailPrivacy": 4,
"dobPrivacy": 0 // Works perfectly with 1
]
do {
let data1 = try JSONSerialization.data(withJSONObject: dict1, options: .prettyPrinted)
let user1 = try JSONDecoder().decode(WebUser.self, from: data1)
print("User1 created")
}
catch DecodingError.dataCorrupted(let context) {
print(context.codingPath)
print(context.debugDescription)
}
catch {
print(error.localizedDescription)
}
I'm using this same Codable WebUser object for Profile detail, search users and many more.
so may be some times one more key will not present in WebRequest's response.
I recommend writing a property wrapper that handles this problem for you.
Specifically, let's write a property wrapper named NilOnDecodingError that turns any DecodingError into nil.
Here's the declaration of NilOnDecodingError:
#propertyWrapper
public struct NilOnDecodingError<Wrapped> {
public init(wrappedValue: Wrapped?) {
self.wrappedValue = wrappedValue
}
public var wrappedValue: Wrapped?
}
We've defined it to wrap any type, storing an Optional.
Now we can conform it to Decodable when the Wrapped type is Decodable:
extension NilOnDecodingError: Decodable where Wrapped: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
wrappedValue = .some(try container.decode(Wrapped.self))
} catch is DecodingError {
wrappedValue = nil
}
}
}
We probably also want it to be Encodable when the Wrapped type is Encodable:
extension NilOnDecodingError: Encodable where Wrapped: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
if let value = wrappedValue {
try container.encode(value)
} else {
try container.encodeNil()
}
}
}
Now we can wrap the appropriate fields of WebUser:
class WebUser: Codable {
let id: String
#NilOnDecodingError
var mobileNumberPrivacy: CommonPrivacyOption?
#NilOnDecodingError
var emailPrivacy: CommonPrivacyOption?
#NilOnDecodingError
var dobPrivacy: CommonPrivacyOption?
}
For testing, we'll want to print the fields of the decoded user:
extension WebUser: CustomStringConvertible {
var description: String {
return """
WebUser(
id: \(id),
mobileNumberPrivacy: \(mobileNumberPrivacy.map { "\($0)" } ?? "nil"),
emailPrivacy: \(emailPrivacy.map { "\($0)" } ?? "nil")),
dobPrivacy: \(dobPrivacy.map { "\($0)" } ?? "nil")))
"""
}
}
Now we can try it out:
let json = """
{
"id": "mrugesh",
"mobileNumberPrivacy": 1,
"emailPrivacy": 2,
"dobPrivacy": 1000
}
"""
let user = try! JSONDecoder().decode(WebUser.self, from: json.data(using: .utf8)!)
print(user)
Output:
WebUser(
id: mrugesh,
mobileNumberPrivacy: privacyOnlyMe,
emailPrivacy: privacyPublic),
dobPrivacy: nil))
You need to create a custom Decodable initializer inside WebUser:
class WebUser: Codable {
var dobPrivacy: CommonPrivacyOption?
// Rest of your properties here.
enum CodingKeys: String, CodingKey {
case dobPrivacy
// Add a case for each property you want to decode here.
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// Use optional try to decode your enum so that when the
// decode fails because of wrong Int value, it will assign nil.
dobPrivacy = try? container.decode(CommonPrivacyOption.self, forKey: .dobPrivacy)
}
}
Alternatively, you can implement the Decodable initializer inside CommonPrivacyOption and add an additional case unknown like so:
enum CommonPrivacyOption: Int, Codable {
case privacyOnlyMe = 1
case privacyPublic, privacyFriends, privacyFriendsOfFriends
case unknown
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let value = try container.decode(Int.self)
// Try to initialize Self from value, if
// value is not 1, 2, 3, or 4, initialize Self to
// the unknown case.
self = .init(rawValue: value) ?? .unknown
}
}
It looks to me like the compiler selects the wrong init for the enum types, instead of init(rawValue) it uses init(from:) that is the one for decoding (which in a way makes sense)
Here is a solution where we override this behaviour by using a custom init(from) in WebUser that decodes the raw values and then creates an enum item
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let value = try container.decodeIfPresent(CommonPrivacyOption.RawValue.self, forKey: .mobileNumberPrivacy), let mobileNumberPrivacy = CommonPrivacyOption(rawValue: value) {
self.mobileNumberPrivacy = mobileNumberPrivacy
}
if let value = try container.decodeIfPresent(CommonPrivacyOption.RawValue.self, forKey: .emailPrivacy), let emailPrivacy = CommonPrivacyOption(rawValue: value) {
self.emailPrivacy = emailPrivacy
}
if let value = try container.decodeIfPresent(CommonPrivacyOption.RawValue.self, forKey: .dobPrivacy), let dobPrivacy = CommonPrivacyOption(rawValue: value) {
self.dobPrivacy = dobPrivacy
}
}
Below is a small example
extension WebUser: CustomStringConvertible {
var description: String {
"Mobile: \(mobileNumberPrivacy?.rawValue), email: \(emailPrivacy?.rawValue), dob: \(dobPrivacy?.rawValue)"
}
}
let data = """
{
"mobileNumberPrivacy": 1,
"dobPrivacy": 0
}
""".data(using: .utf8)!
do {
let result = try JSONDecoder().decode(WebUser.self, from: data)
print(result)
} catch {
print(error)
}
Mobile: Optional(1), email: nil, dob: nil
Of course if you can change your mind about 0 being translated to nil then I would suggest you extend the enum to support the 0 value
enum CommonPrivacyOption: Int, CaseIterable, Codable {
case none = 0
case privacyOnlyMe = 1, privacyPublic, privacyFriends, privacyFriendsOfFriends
}
Then it should work out of the box and you don't need to write any custom code.
I liked #alobaili's answer since it's simple and provided a good solution. One thing I wanted to improve is to make it more generic, so that any Decodable (and Codable) could do it with less code to write.
extension Decodable where Self: RawRepresentable, RawValue: Decodable {
static func initializedOptionalWith(decoder: Decoder, defaultValue: Self) throws -> Self {
let container = try decoder.singleValueContainer()
let value = try container.decode(RawValue.self)
return .init(rawValue: value) ?? defaultValue
}
}
Usage:
init(from decoder: Decoder) throws {
self = try .initializedOptionalWith(decoder: decoder, defaultValue: .unknown)
}
Thank you Rob Mayoff for a great answer. I would like to add just one thing - if the property is missing, decoding fails even if the property is optional. To prevent this failure, add the following extension:
extension KeyedDecodingContainer {
func decode<T>(_ type: NilOnDecodingError<T>.Type, forKey key: Self.Key) throws -> NilOnDecodingError<T> where T: Decodable {
try decodeIfPresent(type, forKey: key) ?? NilOnDecodingError<T>(wrappedValue: nil)
}
}

Skipping empty string with JSONEncoders swift

I a codable serialization extension which I use to turn my Codable struct to dictionaries, the problem I am facing is strings. I get string value from my UITextField at at times this value could be empty and as a result an empty string is decoded. How can I return nil if the value is an empty string.
extension Encodable {
var requestDictionary: [String: Any]? {
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
guard let data = try? encoder.encode(self) else { return nil }
return (try? JSONSerialization.jsonObject(with: data, options: .allowFragments)).flatMap { $0 as? [String: Any] }
}
}
if I have a Struct
let example = Exa(age: 10, name: "")
let dict = example.requestDictionary
print(dict)
I want it to just print ["age": 10] and return nil for the empty string
You can implement your own String encoding method extending KeyedEncodingContainer:
extension KeyedEncodingContainer {
mutating func encode(_ value: String, forKey key: K) throws {
guard !value.isEmpty else { return }
try encodeIfPresent(value, forKey: key)
}
}
Btw your request dictionary can be simplified as:
extension Encodable {
var dictionary: [String: Any]? {
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
return try? JSONSerialization.jsonObject(with: encoder.encode(self)) as? [String: Any]
}
}
Playground testing:
struct Exa: Encodable {
let age: Int
let name: String
}
let example = Exa(age: 10, name: "")
let dict = example.dictionary!
print(dict) // "["age": 10]\n"
I'll just another approach using a property wrapper to mark which properties could be skipped.
#propertyWrapper
struct SkipEmpty {
var wrappedValue: String
}
extension SkipEmpty: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.wrappedValue = try container.decode(String.self)
}
func encode(to encoder: Encoder) throws {
// nothing to do here, see below
}
}
But to actually skip, you'd also need to create a overload for the KeyedEncodingContainer.encode method for the SkipEmpty type:
extension KeyedEncodingContainer {
mutating func encode(_ value: SkipEmpty, forKey key: K) throws {
if !value.wrappedValue.isEmpty {
try encode(value.wrappedValue, forKey: key) // encode the value here
}
}
}
You could possibly try to make it more generic, e.g. SkipEmpty<T: Codable> and provide another argument for the value to skip or a predicate, etc...
The usage is:
struct Exa: Encodable {
var age: Int
#SkipEmpty var name: String
}

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>?
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":"example#ex.com",
"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"}]}
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.

Store Encodables in a Swift Dictionary

I'm looking to store models objects in a Dictionary and would like to serialize the whole dictionary using JSONEncoder into data and subsequently into a string and save it.
The idea is to use Swift 4's out of the box Encodable to ensure anything that I add to the dictionary will be serialized which can include primitives and custom objects (which will themselves conform to Encodable).
The Challenge is what type should I declare the dictionary to be:
If I use [String: Any], it won't know how to encode Any, and if I have to cast it into an actual concrete type, it kind of defeats the purpose of generics
If I use [String: Encodable], it will crash at run time saying
Encodable doesn't conform to itself, which is understandable as it
needs a concrete type
In order to tackle this, I thought of creating a wrapper:
i.e A protocol with an associated type or a struct with generic type value:
struct Serializable<T: Encodable> {
var value: T?
init(value: T) {
self.value = value
}
}
But the problem remains, while declaring the type of the aforementioned dictionary, I still have to supply the concrete type..
var dictionary: [String: Serializable<X>]
What should 'X' be here, Or, what's the correct way to achieve this?
What am I missing?
Two possible approaches:
You can create dictionary whose values are Encodable wrapper type that simply encodes the underlying value:
struct EncodableValue: Encodable {
let value: Encodable
func encode(to encoder: Encoder) throws {
try value.encode(to: encoder)
}
}
Then you can do:
let dictionary = [
"foo": EncodableValue(value: Foo(string: "Hello, world!")),
"bar": EncodableValue(value: Bar(value: 42)),
"baz": EncodableValue(value: "qux")
]
let data = try! JSONEncoder().encode(dictionary)
You can define your own Codable type instead of using dictionary:
struct RequestObject: Encodable {
let foo: Foo
let bar: Bar
let baz: String
}
let requestObject = RequestObject(
foo: Foo(string: "Hello, world!"),
bar: Bar(value: 42),
baz: "qux"
)
let data = try! JSONEncoder().encode(requestObject)
Needless to say, these both assume that both Foo and Bar conform to Encodable.
This is my solution (improved by Rob answer):
struct EncodableValue: Encodable {
let value: Encodable
func encode(to encoder: Encoder) throws {
try value.encode(to: encoder)
}
}
struct Storage: Encodable {
var dict: [String: Encodable] = [:]
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
for (key, value) in dict {
guard let codingKey = CodingKeys(stringValue: key) else {
continue
}
if let enc = value as? EncodableValue {
try container.encode(enc, forKey: codingKey)
}
}
}
struct CodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
return nil
}
}
}
let dict: [String: EncodableValue] = ["test": EncodableValue(value:1), "abc":EncodableValue(value:"GOGO")]
let storage = Storage(dict: dict)
do {
let data = try JSONEncoder().encode(storage)
let res = String(data: data, encoding: .utf8)
print(res ?? "nil")
} catch {
print(error)
}