Firestore. Swift. Create document with dynamic field - swift

I am new to Firestore. For example I have an Object like this:
struct Item: Codable {
let key: String?
let value: ItemAnyValueType?
}
public indirect enum ItemAnyValueType: Codable {
case string(String)
case bool(Bool)
public init(from decoder: Decoder) throws {
let singleValueContainer = try decoder.singleValueContainer()
if let value = try? singleValueContainer.decode(Bool.self) {
self = .bool(value)
return
} else if let value = try? singleValueContainer.decode(String.self) {
self = .string(value)
return
}
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Error"))
}
private enum CodingKeys: String, CodingKey {
case string
case bool
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .string(let value):
try container.encode(value, forKey: .string)
case .bool(let value):
try container.encode(value, forKey: .bool)
}
}
}
The item in Firestore looks like this:
documents
id: 123123 -> key: "test1", value: true
id: 323412 -> key: "test2", value: "good"
id: xx2332 -> key: "test3", value: "i love cats"
id: 433xxx -> key: "test4", value: "i love dogs"
The field value can be boolean or string.
Example of the saved data:
...
...
...
.document("433xxx")
.setData(from: Item(key: "433xxx", value: .string("i love orange")),
merge: true,
completion: { (error) in
print("ok")
})
but the code saves like this:
id: 433xxx -> key: "433xxx", value: map {string: "i love orange"}

Just like you did in your decoder section, you can use a singleValueContainer:
public indirect enum ItemAnyValueType: Codable {
case string(String)
case bool(Bool)
public init(from decoder: Decoder) throws {
let singleValueContainer = try decoder.singleValueContainer()
if let value = try? singleValueContainer.decode(Bool.self) {
self = .bool(value)
return
} else if let value = try? singleValueContainer.decode(String.self) {
self = .string(value)
return
}
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Error"))
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let value):
try container.encode(value)
case .bool(let value):
try container.encode(value)
}
}
}

Related

Decoding JSON in Swift to Dictionary with different types

I have a JSON result from the server that looks like the following:
let json = """
{
"type": "rating",
"data": {
"maxRating": 5,
"isDarkMode": true
}
}
"""
The value for data can be any key-values. I want to map this JSON to my Swift model. So, I implemented the following:
struct Model: Decodable {
let type: String
let data: [String: Any]
private enum CodingKeys: String, CodingKey {
case type
case data
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.type = try container.decode(String.self, forKey: .type)
self.data = try container.decode([String: Any].self, forKey: .data) // THIS
}
}
But on self.data = try container.decode ... it gives me the following error:
error: no exact matches in call to instance method 'decode'
self.data = try container.decode([String: Any].self, forKey: .data)
How can I fix it?
[String : Any] is not decodable. You can not decode with Any. But there have other solutions. Here is one https://dev.to/absoftware/how-to-make-swift-s-string-any-decodable-5c6n
Make change in the Metadata as -
enum DataType: String, Codable {
case payload
case metadata
}
struct Payload: Codable {
let id: String
let eventName: String
let metadata: [Metadata]
}
struct Metadata: Codable {
let maxRating: Int?
let isDarkMode: Bool?
// Add other variables that may appear from your JSON
}
enum MyValue: Decodable {
case payload(_ payload: Payload)
case metadata(_ metadata: Metadata)
private enum CodingKeys: String, CodingKey {
case `type`
case `data`
}
init(from decoder: Decoder) throws {
let map = try decoder.container(keyedBy: CodingKeys.self)
let dataType = try map.decode(DataType.self, forKey: .type)
switch dataType {
case .payload:
self = .payload(try map.decode(Payload.self, forKey: .data))
case .metadata:
self = .metadata(try map.decode(Metadata.self, forKey: .data))
}
}
}
Use the following Extension
JSONCodingKeys.swift
import Foundation
// Inspired by https://gist.github.com/mbuchetics/c9bc6c22033014aa0c550d3b4324411a
struct JSONCodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
self.init(stringValue: "\(intValue)")
self.intValue = intValue
}
}
extension KeyedDecodingContainer {
func decode(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any> {
let container = try self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key)
return try container.decode(type)
}
func decodeIfPresent(_ type: Dictionary<String, Any>.Type, forKey key: K) throws -> Dictionary<String, Any>? {
guard contains(key) else {
return nil
}
return try decode(type, forKey: key)
}
func decode(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any> {
var container = try self.nestedUnkeyedContainer(forKey: key)
return try container.decode(type)
}
func decodeIfPresent(_ type: Array<Any>.Type, forKey key: K) throws -> Array<Any>? {
guard contains(key) else {
return nil
}
return try decode(type, forKey: key)
}
func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
var dictionary = Dictionary<String, Any>()
for key in allKeys {
if let boolValue = try? decode(Bool.self, forKey: key) {
dictionary[key.stringValue] = boolValue
} else if let stringValue = try? decode(String.self, forKey: key) {
dictionary[key.stringValue] = stringValue
} else if let intValue = try? decode(Int.self, forKey: key) {
dictionary[key.stringValue] = intValue
} else if let doubleValue = try? decode(Double.self, forKey: key) {
dictionary[key.stringValue] = doubleValue
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedDictionary
} else if let nestedArray = try? decode(Array<Any>.self, forKey: key) {
dictionary[key.stringValue] = nestedArray
}
}
return dictionary
}
}
extension UnkeyedDecodingContainer {
mutating func decode(_ type: Array<Any>.Type) throws -> Array<Any> {
var array: [Any] = []
while isAtEnd == false {
if let value = try? decode(Bool.self) {
array.append(value)
} else if let value = try? decode(Double.self) {
array.append(value)
} else if let value = try? decode(String.self) {
array.append(value)
} else if let nestedDictionary = try? decode(Dictionary<String, Any>.self) {
array.append(nestedDictionary)
} else if let nestedArray = try? decode(Array<Any>.self) {
array.append(nestedArray)
}
}
return array
}
mutating func decode(_ type: Dictionary<String, Any>.Type) throws -> Dictionary<String, Any> {
let nestedContainer = try self.nestedContainer(keyedBy: JSONCodingKeys.self)
return try nestedContainer.decode(type)
}
}
How to use.
struct Model: Decodable {
let type: String
let data: [String: Any]
private enum CodingKeys: String, CodingKey {
case type
case data
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.type = try container.decode(String.self, forKey: .type)
self.data = try container.decode([String: Any].self, forKey: .data)
}
}
let decoder = JSONDecoder()
let userJson = """
{
"type": "rating",
"data": {
"maxRating": 5,
"isDarkMode": true
}
}
""".data(using: .utf8)!
let user = try! decoder.decode(Model.self, from: userJson)
print(user)
Original answer from
You have to create different struct for data parameter.
You can refer below struct for reference
import Foundation
struct JsonResponse : Codable {
let type : String?
let data : Data?
enum CodingKeys: String, CodingKey {
case type = "type"
case data = "data"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
type = try values.decodeIfPresent(String.self, forKey: .type)
data = try values.decodeIfPresent(Data.self, forKey: .data)
}
}
struct Data : Codable {
let maxRating : Int?
let isDarkMode : Bool?
enum CodingKeys: String, CodingKey {
case maxRating = "maxRating"
case isDarkMode = "isDarkMode"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
maxRating = try values.decodeIfPresent(Int.self, forKey: .maxRating)
isDarkMode = try values.decodeIfPresent(Bool.self, forKey: .isDarkMode)
}
}

Swift force casting to string

i have some data coming. from an api , any way i created a model to transform json to Object.
the problem is that there is a key in json causing my problem.
this key may return an boolean or an string so i need to force cast it to string "i don't have access to the api"
my code :
enum ISpecial: Codable {
case bool(Bool)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(Bool.self) {
self = .bool(x)
return
}
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
throw DecodingError.typeMismatch(ISpecial.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for ISpecial"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .bool(let x):
try container.encode(x)
case .string(let x):
try container.encode(x)
}
}}
then in the view :
VStack(alignment:.leading){
Text(product.name ?? "No name")
.padding(.vertical,5).font(.system(size: 12)).foregroundColor(Color.black.opacity(0.9))
VStack{
Text(product.price ?? " ").strikethrough()
Text( product.special ?? " ")
}
}
You can add a property to ISpecial that returns a String if it is the string case, and nil otherwise:
var stringValue: String? {
if case .string(let s) = self {
return s
} else {
return nil
}
}
Then you can do:
Text( product.special?.stringValue ?? " ")

Codable conformance for nested Enum?

I'm a bit tangled up on using Codable with nested enums. Suppose I have the following enum:
enum ItemStatus {
enum Foo {
case a
case b(Double, Double)
case c
}
enum Bar {
case x(String)
case y
case z(Int)
}
}
extension ItemStatus: Codable {
init(from decoder: Decoder) throws {
}
func encode(to encoder: Encoder) throws {
}
}
For normal enums, I'm implementing Codable like this. It works, but I'm not sure how to implement Codable for the top-level enum (in this case, ItemStats), and my gut tells me there is a more efficient way to write this anyway:
extension ItemStatus.Bar {
init?(rawValue: String?) {
guard let val = rawValue?.lowercased() else {
return nil
}
switch val {
case "x": self = .x("")
case "y": self = .y
case "z": self = .z(0)
default: return nil
}
}
private var typeStr: String {
guard let label = Mirror(reflecting: self).children.first?.label else {
return .init(describing: self)
}
return label
}
}
extension ItemStatus.Bar : Codable {
private enum CodingKeys: String, CodingKey {
case type
case xVal
case zVal
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
guard let type = try? container.decode(String.self, forKey: .type) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath, debugDescription: "Could not get type of Bar."))
}
guard let base = ItemStatus.Bar(rawValue: type) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: container.codingPath, debugDescription: "Could not init Bar with value: \(type)"))
}
switch base {
case .x:
let val = try container.decode(String.self, forKey: .xVal)
self = .x(val)
case .z:
let val = try container.decode(Int.self, forKey: .zVal)
self = .z(val)
default:
self = base
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.typeStr, forKey: .type)
switch self {
case .x(let val):
try container.encode(val, forKey: .xVal)
case .z(let val):
try container.encode(val, forKey: .zVal)
default: ()
}
}
}

Swift Codable how to use Any type?

When I try to access the value of "value" for example to use it in a label.text, I get an error
Cannot assign value of type 'MyValue?' to type 'String?'
When I print the value to the terminal, it says ...
unknown context at 0x109d06188).MyValue.string...
How can solve this problem?
struct Root: Codable {
let description,id: String
let group,groupDescription: String?
let name: String
let value: MyValue
enum CodingKeys: String, CodingKey {
case description = "Description"
case group = "Group"
case groupDescription = "GroupDescription"
case id = "Id"
case name = "Name"
case value = "Value"
}
}
enum MyValue: Codable {
case string(String)
case innerItem(InnerItem)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
if let x = try? container.decode(InnerItem.self) {
self = .innerItem(x)
return
}
throw DecodingError.typeMismatch(MyValue.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for MyValue"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let x):
try container.encode(x)
case .innerItem(let x):
try container.encode(x)
}
}
}
You can get string values for your label by conforming to rawRepresentable protocol:
enum MyValue: Codable, RawRepresentable {
var rawValue: String {
switch self {
case .string(let stringVal):
return stringVal
case .innerItem(let myVal):
return String(describing: myVal)
}
}
typealias RawValue = String
init?(rawValue: String) {
return nil
}
case string(String)
case innerItem(InnerItem)
}
let myVal = MyValue.string("testString")
var strVal: String = myVal.rawValue // testString
To get the associated values in the enum you could add two computed properties in MyValue
var stringValue : String? {
guard case .string(let string) = self else { return nil }
return string
}
var innerItemValue : InnerItem? {
guard case .innerItem(let innerItem) = self else { return nil }
return innerItem
}
Or switch on value like in the encode method
switch root.value {
case .string(let string): // do something with `string`
case .innerItem(let innerItem): // do something with `innerItem`
}
Or simply use if case
if case .string(let string) = root.value { someLabel.text = string }

How to parse JSON with Decodable protocol when property types might change from Int to String? [duplicate]

This question already has answers here:
Using codable with value that is sometimes an Int and other times a String
(5 answers)
Closed 6 months ago.
I have to decode a JSON with a big structure and a lot of nested arrays.
I have reproduced the structure in my UserModel file, and it works, except with one property (postcode) that is in a nested array (Location) that sometimes is an Int and some other is a String. I don't know how to handle this situation and tried a lot of different solutions.
The last one I've tried is from this blog https://agostini.tech/2017/11/12/swift-4-codable-in-real-life-part-2/
And it suggests using generics. But now I can't initialize the Location object without providing a Decoder():
Any help or any different approach would be appreciated.
The API call is this one: https://api.randomuser.me/?results=100&seed=xmoba
This is my UserModel File:
import Foundation
import UIKit
import ObjectMapper
struct PostModel: Equatable, Decodable{
static func ==(lhs: PostModel, rhs: PostModel) -> Bool {
if lhs.userId != rhs.userId {
return false
}
if lhs.id != rhs.id {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.body != rhs.body {
return false
}
return true
}
var userId : Int
var id : Int
var title : String
var body : String
enum key : CodingKey {
case userId
case id
case title
case body
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: key.self)
let userId = try container.decode(Int.self, forKey: .userId)
let id = try container.decode(Int.self, forKey: .id)
let title = try container.decode(String.self, forKey: .title)
let body = try container.decode(String.self, forKey: .body)
self.init(userId: userId, id: id, title: title, body: body)
}
init(userId : Int, id : Int, title : String, body : String) {
self.userId = userId
self.id = id
self.title = title
self.body = body
}
init?(map: Map){
self.id = 0
self.title = ""
self.body = ""
self.userId = 0
}
}
extension PostModel: Mappable {
mutating func mapping(map: Map) {
id <- map["id"]
title <- map["title"]
body <- map["body"]
userId <- map["userId"]
}
}
Well it's a common IntOrString problem. You could just make your property type an enum that can handle either String or Int.
enum IntOrString: Codable {
case int(Int)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
self = try .int(container.decode(Int.self))
} catch DecodingError.typeMismatch {
do {
self = try .string(container.decode(String.self))
} catch DecodingError.typeMismatch {
throw DecodingError.typeMismatch(IntOrString.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Encoded payload conflicts with expected type, (Int or String)"))
}
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .int(let int):
try container.encode(int)
case .string(let string):
try container.encode(string)
}
}
}
As I have found mismatch of your model that you posted in your question and the one in the API endpoint you pointed to, I've created my own model and own JSON that needs to be decoded.
struct PostModel: Decodable {
let userId: Int
let id: Int
let title: String
let body: String
let postCode: IntOrString
// you don't need to implement init(from decoder: Decoder) throws
// because all the properties are already Decodable
}
Decoding when postCode is Int:
let jsonData = """
{
"userId": 123,
"id": 1,
"title": "Title",
"body": "Body",
"postCode": 9999
}
""".data(using: .utf8)!
do {
let postModel = try JSONDecoder().decode(PostModel.self, from: jsonData)
if case .int(let int) = postModel.postCode {
print(int) // prints 9999
} else if case .string(let string) = postModel.postCode {
print(string)
}
} catch {
print(error)
}
Decoding when postCode is String:
let jsonData = """
{
"userId": 123,
"id": 1,
"title": "Title",
"body": "Body",
"postCode": "9999"
}
""".data(using: .utf8)!
do {
let postModel = try JSONDecoder().decode(PostModel.self, from: jsonData)
if case .int(let int) = postModel.postCode {
print(int)
} else if case .string(let string) = postModel.postCode {
print(string) // prints "9999"
}
} catch {
print(error)
}
You can use generic like this:
enum Either<L, R> {
case left(L)
case right(R)
}
extension Either: Decodable where L: Decodable, R: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let left = try? container.decode(L.self) {
self = .left(left)
} else if let right = try? container.decode(R.self) {
self = .right(right)
} else {
throw DecodingError.typeMismatch(Either<L, R>.self, .init(codingPath: decoder.codingPath, debugDescription: "Expected either `\(L.self)` or `\(R.self)`"))
}
}
}
extension Either: Encodable where L: Encodable, R: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case let .left(left):
try container.encode(left)
case let .right(right):
try container.encode(right)
}
}
}
And then declare postcode: Either<Int, String> and if your model is Decodable and all other fields are Decodable too no extra code would be needed.
If postcode can be both String and Int, you have (at least) two possible solutions for this issue. Firstly, you can simply store all postcodes as String, since all Ints can be converted to String. This seems like the best solution, since it seems highly unlikely that you'd need to perform any numeric operations on a postcode, especially if some postcodes can be String. The other solution would be creating two properties for postcode, one of type String? and one of type Int? and always only populating one of the two depending on the input data, as explained in Using codable with key that is sometimes an Int and other times a String.
The solution storing all postcodes as String:
struct PostModel: Equatable, Decodable {
static func ==(lhs: PostModel, rhs: PostModel) -> Bool {
return lhs.userId == rhs.userId && lhs.id == rhs.id && lhs.title == rhs.title && lhs.body == rhs.body
}
var userId: Int
var id: Int
var title: String
var body: String
var postcode: String
enum CodingKeys: String, CodingKey {
case userId, id, title, body, postcode
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.userId = try container.decode(Int.self, forKey: .userId)
self.id = try container.decode(Int.self, forKey: .id)
self.title = try container.decode(String.self, forKey: .title)
self.body = try container.decode(String.self, forKey: .body)
if let postcode = try? container.decode(String.self, forKey: .postcode) {
self.postcode = postcode
} else {
let numericPostcode = try container.decode(Int.self, forKey: .postcode)
self.postcode = "\(numericPostcode)"
}
}
}
try this extension
extension KeyedDecodingContainer{
public func decodeIfPresent(_ type: String.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> String?{
if let resStr = try? decode(type, forKey: key){
return resStr
}else{
if let resInt = try? decode(Int.self, forKey: key){
return String(resInt)
}
return nil
}
}
public func decodeIfPresent(_ type: Int.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> Int?{
if let resInt = try? decode(type, forKey: key){
return resInt
}else{
if let resStr = try? decode(String.self, forKey: key){
return Int(resStr)
}
return nil
}
}
}
example
struct Foo:Codable{
let strValue:String?
let intValue:Int?
}
let data = """
{
"strValue": 1,
"intValue": "1"
}
""".data(using: .utf8)
print(try? JSONDecoder().decode(Foo.self, from: data!))
it will print "Foo(strValue: Optional("1"), intValue: Optional(1))"