Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

request.content.decode should treat empty strings for optionals as nil #3046

Open
grey280 opened this issue Jul 31, 2023 · 10 comments
Open

request.content.decode should treat empty strings for optionals as nil #3046

grey280 opened this issue Jul 31, 2023 · 10 comments
Labels
bug Something isn't working

Comments

@grey280
Copy link

grey280 commented Jul 31, 2023

Describe the bug

I've got a custom Codable type that throws an error when decoding... because it's being asked to decode an empty string, rather than being skipped because I've got T? rather than T as the type in my Content.

To Reproduce

  1. Declare some custom Codable type that throws an error if told to decode from an empty string.
  2. Declare some custom Content struct that has an Optional of your custom type as a parameter.
  3. Have a browser submit a request that includes yourThing: in the body. (Doable in Safari by creating an HTML form, giving it a date input with the right name, and then... not selecting a date before hitting submit)
  4. Observe thrown error.

Expected behavior

Vapor should see that it was given a blank value and decode that as nil, not tell my custom type to try to decode.

Environment

  • Vapor Framework version: 4.76.3
  • Vapor Toolbox version: 18.7.1
  • OS version: macOS 13.5

Additional context

I suspect this is the same kind of thing as #2460, and that a similar fix to #2463 would work - modifying URLEncodedFormDecoder._Decoder.KeyedContainer.decodeNil(forKey:) to also check for empty values ought to do it? (I did some poking at it in the debugger, and ... == nil || (self.data.children[key.stringValue]?.values.count == 1 && self.data.children[key.stringValue]?.values.first == .urlDecoded("")) worked right, but feels a little clunky.)

@grey280 grey280 added the bug Something isn't working label Jul 31, 2023
@MahdiBM
Copy link
Contributor

MahdiBM commented Jul 31, 2023

If i understand correctly, you basically want a nil value to be decoded as just empty string like ""?

I realize this is not nice to work around, but i don't think adding this as the default behavior is a good idea, considering it will be inconsistent with the usual Codable behavior when using JSONDecoder etc...

You can use a custom init(from:) method, or declare a new struct named NullableString with a init(from:) method that does let container = try decoder.singleValueContainer() then self.value = container.decode(String.self) assuming the struct contains var value: String.

There are a number of other workarounds to this problem too, like using property wrappers, which is more complicated, see https://gist.github.com/MahdiBM/260a6551f4b9a1af1994f44e305c313e as an example.

@grey280
Copy link
Author

grey280 commented Jul 31, 2023

The opposite - I want an empty string to be decoded as nil, as that’s how Safari, at least, transfers “this field was not touched by the user prior to submitting the form.” If that isn’t nil, I don’t know what is.

@MahdiBM
Copy link
Contributor

MahdiBM commented Jul 31, 2023

Ah OK.

But again, this isn't something that a Decoder should care about.

If we change it to turn empty strings to nil, there might be another user complaining than they would like the original empty string.

Other than the original argument which is that this is inconsistent behavior with all the other decoders in the ecosystem.

I think you should try to just check with 'string.isEmpty'. You are not losing any information about the string either.

@grey280
Copy link
Author

grey280 commented Jul 31, 2023

Yes, but now I go from “implement Codable by adding : Codable to my type” to “I have to implement init(from: Decoder) and fill it with boilerplate for everything that uses my type.” It’s like a 5% increase in the total lines of code in my entire project in order to support forms, and that doesn’t feel like something that should be a requirement for a server framework.

Maybe a configuration option to enable this behavior?

@MahdiBM
Copy link
Contributor

MahdiBM commented Jul 31, 2023

I'm not against a configuration option if it'll be very useful 🤔

I still think you could try to use something like a property wrapper.
Unfortunately it'll be hard to get right, but at least you have some code that already does that, just for another purpose (the gist link I mentioned above)

There are some new Codable macros and stuff like that with Swift 5.9 getting closer to release. On a personal level I hope they can solve some of these issues with Codable sometimes being too uncustomizable.

@grey280
Copy link
Author

grey280 commented Jul 31, 2023

If my read of the gist is correct, it’s doing the opposite of what I want - i.e., given a String? as the thing to decode, if it encounters a blank it decodes it as ”” rather than nil.

@MahdiBM
Copy link
Contributor

MahdiBM commented Jul 31, 2023

What the gist does is decodes "" for String even if the e.g. json value is null. (so no need for declaring your variable as String?).
And yes i do recognize that's not what you want, I'm just saying it's something you can try to get help from 🙂

@henrikac
Copy link

henrikac commented Aug 5, 2023

Maybe a configuration option to enable this behavior?

I'm not sure but from what I am reading this looks like it might be what you're looking for? :)

https://docs.vapor.codes/basics/content/#global

@grey280
Copy link
Author

grey280 commented Aug 6, 2023

I think I could do that, yes, but I'm still of the opinion that a web server framework should support "POSTing a <form> from a web browser" without boilerplate in every init(from decoder) or writing my own custom Decoder that differs from the shipping one, at the time I write it, by one line.

@zygoat
Copy link

zygoat commented Jan 15, 2024

I'm faced with the same problem: I need to handle a callback from a third-party service that insists on sending keys with null values rather than excluding them. These payload contains a variety of types (strings, ints, dates, etc.) and therefore I seem to be stymied. (URLEncodedFormDecoder is a struct so can't be subclassed, and its internal machinery is all private so it's impossible to make the adjustment in any case.)

Update: in the mean time I've worked around this by creating a middleware that strips out such empty entries from the request which I attach to the affected endpoints.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

4 participants