mattpolzin / JSONAPI

Swift Codable JSON:API framework
MIT License
75 stars 19 forks source link

Optional Attributes #99

Closed scotsimon closed 2 years ago

scotsimon commented 2 years ago

Let me first say how much I am loving your JSONAPI framework! What amazing work! Also, thanks for resolving that minor issue with the Cocoapods install.

I am working with a server object that has an auto-generated field value. I need to be able to retrieve the value as an attribute for GET requests, but I cannot include it in the json body for POST requests. I was assuming that Optional Attributes were the solution to this issue, but I'm having trouble making those work without generating errors in XCode.

I've defined the attribute as: let origin: Attribute?

I can pass nil into the value without any problems (for POST requests), but when I attempt to use a value for the attribute I get the following error:

**Cannot convert value of type 'Attribute<String?>' to expected argument type 'Attribute[String]'

Note: The GitHub editor won't let me type angle brackets in the second quotation for some reason? So I used square brackets.

I was hoping you might have some sample code or additional information that might help me debug this issue.

Thanks! Scot

mattpolzin commented 2 years ago

I can think of two ways you might want to accomplish this. If you want the result of your response payloads to represent the fact that the attribute in question is non-null (i.e. GET requests always guarantee that the value exists and is not nil), you may need to create two different resource descriptions -- one with that attribute and one without it; the second one for POST requests.

However, if you want to stick to one resource description used for both, but simply omit the attribute in question on POST requests, it sounds like you want to use an optional attribute (as you mentioned in your original question). You'll need to be careful in this situation to use an optional attribute rather than a nullable attribute. The former can be completely omitted from payloads whereas the latter must exist in the payload but can be set to nil.

Here's a bit of code that might help you and illustrate the difference I am talking about:

    // attr1 is optional (can be omitted entirely)
    enum WidgetDescription: ResourceObjectDescription {
        public static var jsonType: String { "widgets" }

        public struct Attributes: JSONAPI.Attributes {
            public let attr1 : Attribute<String>? // <- question mark after square brackets
        }
        public typealias Relationships = NoRelationships
    }

    // the following:
    let tmp = JSONAPI.ResourceObject<WidgetDescription, NoMetadata, NoLinks, String>(id: "1234", attributes: .init(attr1: nil), relationships: .none, meta: .none, links: .none)
    try! print(String(data: JSONEncoder.init().encode(tmp), encoding: .utf8)!)

    // will print:
    // {"type":"widgets","id":"1234","attributes":{}}
    // attr1 is nullable (cannot be omitted, but can be set to `nil`)
    enum WidgetDescription: ResourceObjectDescription {
        public static var jsonType: String { "widgets" }

        public struct Attributes: JSONAPI.Attributes {
            public let attr1 : Attribute<String?> // <- question mark inside square brackets
        }
        public typealias Relationships = NoRelationships
    }

    // the following:
    let tmp = JSONAPI.ResourceObject<WidgetDescription, NoMetadata, NoLinks, String>(id: "1234", attributes: .init(attr1: nil), relationships: .none, meta: .none, links: .none)
    try! print(String(data: JSONEncoder.init().encode(tmp), encoding: .utf8)!)

    // will print:
    // {"type":"widgets","id":"1234","attributes":{"attr1":null}}

Take a close look at the two code snippets; the only difference is the location of a single question mark in the type of the attribute. In the case where the attribute will be completely omitted when nil, the question mark is outside the angle brackets.

mattpolzin commented 2 years ago

Upon taking a second look at your question, you actually appear to have already been using the question mark in the correct location for an optional (omitted) attribute; I was thrown off by GitHub clobbering a pair of your angle brackets on your first line:

I've defined the attribute as: let origin: Attribute?

Anyway, maybe the following extension of my first couple of examples will shed light on the solution to your problem. Assuming you use the question mark outside the brackets as in my first example above, the following code:

        let tmp = JSONAPI.ResourceObject<WidgetDescription, NoMetadata, NoLinks, String>(
            id: "1234",
            attributes: .init(
                attr1: .init(value: "hello")
            ),
            relationships: .none,
            meta: .none,
            links: .none
        )
        try! print(String(data: JSONEncoder.init().encode(tmp), encoding: .utf8)!)

Will assign a non-nil value to the attr1 attribute. The serialized JSON is:

{"type":"widgets","id":"1234","attributes":{"attr1":"hello"}}

Perhaps the part that was missing from your code was that an attribute needs to be wrapped in an extra init call. Note above how instead of writing attr1: "hello" I must write attr1: .init(value: "hello").

scotsimon commented 2 years ago

Matt,

This is great feedback - thank you!

After looking at this last night, I decided that the best option was to use separate resource descriptions for the GET and POST functions. But, I'm definitely going to keep this sample code on hand for the future.

I want to say how much I'm enjoying the well-designed structure of your code. There was a bit of a learning curve, but after that it provides a structured efficient way of parsing data to/from the JSON:API format.

Thanks! Scot