swiftencodable

Custom encoding of a struct


I'm trying to map the following SignUpRequest struct

struct SignUpRequest: Encodable {

    struct Customer: Encodable {
        let email: String?
        let firstname: String?
        let lastname: String?
        let privacy: Bool
        let privacy2: Bool
    }

    let customer: Customer
    let password: String?

}

in a JSON like this:

{
    "customer": {
        "email": "mike.m@yopmail.com",
        "firstname": "Mario",
        "lastname": "Rossi",
        "custom_attributes":[
            {
                "attribute_code":"consensopubb",
                "value":"3"
            },
            {
                "attribute_code":"consensopubb2",
                "value":"5"
            }
        ]
    },
    "password": "Password1!"
}

I tried to create a custom encoding of my struct.

struct SignUpRequest: Encodable {

    let customer: Customer
    let password: String?

    struct Customer: Encodable {

        let email: String?
        let firstname: String?
        let lastname: String?
        let privacy: Bool
        let privacy2: Bool

        enum CodingKeys: String, CodingKey {
            case email
            case firstname
            case lastname
            case attributes = "custom_attributes"
        }

        enum AttributeCodingKeys: String, CodingKey {
            case code = "attribute_code"
            case value
        }

        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)

            try container.encode(email, forKey: .email)
            try container.encode(firstname, forKey: .firstname)
            try container.encode(lastname, forKey: .lastname)

            // ...

        }

    }

}

My problem is that I can't figure how to encode privacy1 and privacy2 properties into JSON custom_attributes array.

-- EDIT --

privacy1 and privacy2 properties are Bool because map two checkbox that user can check or not. Unfortunately API wants an array of custom_attributes. consensopubb with value "3" if privacy1 is true. consensopubb2 with value "5" if privacy2 is true.


Solution

  • A possible solution:

    struct SignUpRequest: Encodable {
        let customer: Customer
        let password: String?
    }
    
    extension SignUpRequest {
        
        struct Customer: Encodable {
            let email: String?
            let firstname: String?
            let lastname: String?
            let privacy1: Bool
            let privacy2: Bool
            
            enum CustomerCodingKeys: String, CodingKey {
                case email
                case firstname
                case lastname
                case attributes = "custom_attributes"
            }
            
            func encode(to encoder: Encoder) throws {
                var container = encoder.container(keyedBy: CustomerCodingKeys.self)
                var codes: [AttributeCode] = []
                if privacy1 {
                    codes.append(AttributeCode(value: "3", code: "consensopubb"))
                }
                if privacy2 {
                    codes.append(AttributeCode(value: "5", code: "consensopubb2"))
                }
                try container.encode(codes, forKey: .attributes)
                try container.encode(email, forKey: .email)
                try container.encode(firstname, forKey: .firstname)
                try container.encode(lastname, forKey: .lastname)
            }
        }
    }
    extension SignUpRequest.Customer {
        struct AttributeCode: Encodable {
            
            let value: String
            let code: String
            
            enum AttributeCodingKeys: String, CodingKey {
                case code = "attribute_code"
                case value
            }
        }
    }
    

    Testing with:

    let requests: [SignUpRequest] = [SignUpRequest(customer: .init(email: "email1", 
                                                                   firstname: "f1",
                                                                   lastname: "l1",
                                                                   privacy1: true,
                                                                   privacy2: true),
                                                   password: "p1"),
                                     SignUpRequest(customer: .init(email: "email2", 
                                                                   firstname: "f2",
                                                                   lastname: "l2",
                                                                   privacy1: false,
                                                                   privacy2: true),
                                                   password: "p2"),
                                     SignUpRequest(customer: .init(email: "email3", 
                                                                   firstname: "f3",
                                                                   lastname: "l3",
                                                                   privacy1: true,
                                                                   privacy2: false),
                                                   password: "p3"),
                                     SignUpRequest(customer: .init(email: "email4", 
                                                                   firstname: "f4",
                                                                   lastname: "l4",
                                                                   privacy1: false,
                                                                   privacy2: false),
                                                   password: "p4")]
    
    
    requests.forEach { aRequest in
        do {
            let encoder = JSONEncoder()
            encoder.outputFormatting = [.prettyPrinted, .sortedKeys] //For debug purposes
            let data = try JSONEncoder().encode(aRequest)
            let jsonString = String(data: data, encoding: .utf8)!
            print(jsonString)
        } catch {
            print("Error: \(error)")
        }
    }
    

    Now, I created a custom AttributeCode struct to make thing easier. Of course, you could customize all the encode and avoid that struct (which is pretty easy since it's a [String: String], but I feel that sometimes if the encode(to:) if too difficult for the user (for later debug, modifications), or the encode(to:) isn't clear enough at a quick glance, it a lot better to have explicit extra struct, you can make it private if needed and it can only be seen by SignUpRequest.Customer.

    I made the custom encode on Customer, but indeed, it can be done with other modifications on SignUpRequest if only that one needs that special encoding and you don't want to modify the encoding of Customer (if it's not done through higher SignUpRequest), but with this should you have the whole idea.

    Edit: If you don't really want AttributedCode struct, you can write:

    var codes: [[String: String]] = []
    if privacy1 {
        codes.append(["value": "3", "attribute_code": "consensopubb"])
    }
    if privacy2 {
        codes.append(["value": "5", "attribute_code": "consensopubb2"])
    }
    try container.encode(codes, forKey: .attributes)
    

    It it was a [String: Any] it'd be more complex, and I'd still suggest to have the AttributedCode struct as it's easier to read at first glance, but both solution works and are valid.