Welcome to the Treehouse Community

The Treehouse Community is a meeting place for developers, designers, and programmers of all backgrounds and skill levels to get support. Collaborate here on code errors or bugs that you need feedback on, or asking for an extra set of eyes on your latest project. Join thousands of Treehouse students and alumni in the community today. (Note: Only Treehouse students can comment or ask questions, but non-students are welcome to browse our conversations.)

Looking to learn something new?

Treehouse offers a seven day free trial for new students. Get access to thousands of hours of content and a supportive community. Start your free trial today.

iOS Parsing JSON Using Codable Parsing Different JSON Structures Missing Data

Garrett Votaw
seal-mask
.a{fill-rule:evenodd;}techdegree seal-36
Garrett Votaw
iOS Development Techdegree Graduate 15,223 Points

Nil values not encoded on Xcode Version 9.3.1

I'm not able to encode nil values into JSON so that the key has a value of null. I'm using the latest version of Xcode 9.3.1 and here is my code

let json = """
{
    "name": null,
    "id": 1,
    "role": "Teacher",
    "start_date":"April 2012"
}
""".data(using: .utf8)!

struct Employee: Encodable, Decodable {
    let name: String?
    let id: Int
    let role: String
    let startDate: String

    enum CodingKeys: String, CodingKey {
        case name
        case id
        case role
        case startDate = "start_date"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String?.self, forKey: .name)
        self.id = try container.decode(Int.self, forKey: .id)
        self.role = try container.decode(String.self, forKey: .role)
        self.startDate = try container.decode(String.self, forKey: .startDate)
    }

    func encode(with encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encode(id, forKey: .id)
        try container.encode(role, forKey: .role)
        try container.encode(startDate, forKey: .startDate)
    }
}


let decoder = JSONDecoder()
let employee = try! decoder.decode(Employee.self, from: json)
employee.name
employee.id

let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
let encodedEmployee = try! encoder.encode(employee)
print(encodedEmployee.stringDescription)

//output is: {"id":1,"role":"Teacher","start_date":"April 2012"}
//expected output: {"name":null,"id":1,"role":"Teacher","start_date":"April 2012"}

Any ideas would be great!

Xavier D
Xavier D
Courses Plus Student 5,840 Points

Hi.

Ya know...sometimes I like to read video posts prior to watching the video to get some heads up on what may be difficult in the video...and that's what I did with your post!

After watching Pasan, I saw that there was a way to null displayed in the results without it being a string and surrounded with quotation marks, and I found that your response here is similar to Pasan's, however, null is not showing in the output.

I was at first, really lost with the magic of the null value not appearing in your output, and as well as not appearing in my output when using your code as a template.

Thus, since Pasan's code is the correct working one, I decided to use his code as a template, and compared it side-by-side with yours (luckily, I have a dual/triple monitor setup...the real estate can really come in handy in situations like this. I ended up removing the extensions like you did, did a lot of editing in scope, and then copied and pasted his code onto your equivalent code one block at a time, and running the playground each time until I saw null in the output.

Then I found the problem once I got to the encode function...and it has something to do with the parameter that you you chose, not sure why the issue is the parameter, but maybe now I can agree with you that it is a bug!

So....when I pasted Pasan's encode method on top of yours, the only thing that changed was the with parameter, which changed to to--the statement with the assignment to the variable and the try statements remained unchanged. After that modification and running playground again, the null value and its name parameter appeared n the output.

Therefore, when defining your encode function, make sure to use the word to but the first parameter, not the word with.

Change with to to in your code and run it to see what I mean.

XD

5 Answers

Scott Ho
Scott Ho
288 Points

use container.encodeIfPresent

Xavier D
PLUS
Xavier D
Courses Plus Student 5,840 Points

Hi. Would this work for you? I'm using Xcode 10.1 though...

import Foundation

import Foundation
let json = """
{
    "name": null,
    "id": 1,
    "role": "Teacher",
    "start_date":"April 2012"
}
""".data(using: .utf8)!

struct Employee: Encodable, Decodable {
    let name: String?
    let id: Int
    let role: String
    let startDate: String

    enum CodingKeys: String, CodingKey {
        case name
        case id
        case role
        case startDate = "start_date"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String?.self, forKey: .name) ?? "null"
        self.id = try container.decode(Int.self, forKey: .id)
        self.role = try container.decode(String.self, forKey: .role)
        self.startDate = try container.decode(String.self, forKey: .startDate)
    }

    func encode(with encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encode(id, forKey: .id)
        try container.encode(role, forKey: .role)
        try container.encode(startDate, forKey: .startDate)
    }
}


let decoder = JSONDecoder()
let employee = try! decoder.decode(Employee.self, from: json)
employee.name
employee.id

let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
let encodedEmployee = try! encoder.encode(employee)
print(encodedEmployee.stringDescription)


// output is: {name":"null","id":1,","role":"Teacher","start_date":"April 2012"}

Also works when I replace null with a string...

import Foundation

import Foundation
let json = """
{
    "name": "Xavier",
    "id": 1,
    "role": "Teacher",
    "start_date":"April 2012"
}
""".data(using: .utf8)!

struct Employee: Encodable, Decodable {
    let name: String?
    let id: Int
    let role: String
    let startDate: String

    enum CodingKeys: String, CodingKey {
        case name
        case id
        case role
        case startDate = "start_date"
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.name = try container.decode(String?.self, forKey: .name) ?? "null"
        self.id = try container.decode(Int.self, forKey: .id)
        self.role = try container.decode(String.self, forKey: .role)
        self.startDate = try container.decode(String.self, forKey: .startDate)
    }

    func encode(with encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(name, forKey: .name)
        try container.encode(id, forKey: .id)
        try container.encode(role, forKey: .role)
        try container.encode(startDate, forKey: .startDate)
    }
}


let decoder = JSONDecoder()
let employee = try! decoder.decode(Employee.self, from: json)
employee.name
employee.id

let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
let encodedEmployee = try! encoder.encode(employee)
print(encodedEmployee.stringDescription)

// output is: {name":"Xavier","id":1,","role":"Teacher","start_date":"April 2012"}

Thus, I guess whether there’s a value of a string literal or null for the name parameter of the JSON in the json constant, the code works, at least in Xcode 10.1

The only thing I noticed that differed from your expected output is the quotations surrounding the null value in my output…and I have two things to say about that…

  1. I guess there’s quotation marks around the null value because, even though that value is not represented as a string literal in the JSON dictionary, the entire JSON itself that’s in the json constant is within the double triple quotes, which makes the entire example a string that’s partitioned by many lines…or because that the nil-coalescing operator expects that a non-nil value of a type equivalent to the optional is explicitly provided…perhaps the latter since I assume the decoder treats json as actual JSON…but no actually … because we’re encoding for that output, in which a constant (not json) is used as an argument, and it’s actually because employee really had a string for name to begin with, due to the nil-coalescing operator causing the compiler to swap out the missing value with a string literal during decoding….so I guess prior to my revisions, during decoding of your code, the object that you had created using the struct as the blueprint has the name to be nil, which is the absence of a value, and since the pertaining member of the struct is an optional type, that value was valid but since doesn’t exist, the property also didn’t exist when you used the object it belonged to in order to encode back to JSON.

  2. From my experience in Swift, any optional type that was nil in a function call or when creating an instance, the pertaining parameter of the type would not show up as an output because I guess for the simple fact that a value for it is not there—it doesn’t exist. I guess that’s because those kinds of types are optional types, thus they are optionally present if they have non-absent values. Thus I don't think it's a bug--Swift is handling the nil values as how it should, I think. The struct indicates name as optional, and since a non-existing value is associated with it, it also doesn't exist...just like its value doesn't exist.

I am kind of curious of why would it be necessary for the needed output to contain the name parameter with a null value when no value for it exists. Being missing in the output would convey that name is missing because there’s no value for it, would it not? If the null value has to be explicit regardless, would it matter that quotations surround the value in the output?

XD

Xavier D
PLUS
Xavier D
Courses Plus Student 5,840 Points

This solution will give you your expected output

import Foundation

let json = """
{
        "name": null,
        "id": 1,
        "role": "Teacher",
        "start_date":"April 2012"
}
""".data(using: .utf8)!

struct Employee: Decodable, Encodable
{
    let name: String?
    let id: Int
    let role: String
    let startDate: String

    enum CodingKeys: String, CodingKey {case name, id, role, startDate}

    init(from decoder: Decoder) throws
    {
        let container = try decoder.container(keyedBy: CodingKeys.self)
            self.name = try container.decode(String?.self, forKey: .name)
            self.id = try container.decode(Int.self, forKey: .id)
            self.role = try container.decode(String.self, forKey: .role)
            self.startDate = try container.decode(String.self, forKey: .startDate)
    }

    func encode(to encoder: Encoder) throws
    {
        var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(name, forKey: .name)
            try container.encode(id, forKey: .id)
            try container.encode(role, forKey: .role)
            try container.encode(startDate, forKey: .startDate)
    }
}

let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .convertFromSnakeCase

let employee = try! decoder.decode(Employee.self, from: json)
    employee.name
    employee.id

let encoder = JSONEncoder()
    encoder.keyEncodingStrategy = .convertToSnakeCase
        let encodedEmployee = try! encoder.encode(employee)
            print(encodedEmployee.stringDescription)

// output is: {"name":null,"id":1,"role":"Teacher","start_date":"April 2012"}

Very strange how the parameter name is affect the output, even though that parameter is not even being used in any of the method calls... a Swift expert is definitely needed to explain this issue...

XD

You can not encode nil value as null with default JSONEncoder.

According to this link

You should write your own encoder.