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

feat: Serialize objects #10

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open

feat: Serialize objects #10

wants to merge 8 commits into from

Conversation

razor-x
Copy link
Member

@razor-x razor-x commented Feb 24, 2025

  • docs: Update example in README
  • docs: Add Date and Temporal to examples
  • Add float to examples
  • Remove test.only
  • feat: Serialize plain objects to dot path notation
  • doc: Note serialization guarantee

@razor-x razor-x mentioned this pull request Feb 24, 2025
@razor-x razor-x marked this pull request as ready for review February 24, 2025 22:54
- Serialization of objects and nested objects first serializes the keys
to dot-path format and then serializes the values as above, e.g.,
`{ foo: 'a', bar: { baz: 'b', fizz: [1, 2] } }` serializes to
`foo=a&bar.baz=b&bar.fizz=1&bar.fizz=2`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you consider using PHP style here? That is:

?foo=a&bar[baz]=b&bar[fizz][]=1&bar[fizz][]=2

Copy link
Member Author

@razor-x razor-x Feb 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I did not directly consider it, but thanks for the suggestion.

I chose dot notation because it's the simplest to read, write and parse (and does not need escaping in other contexts like the command line).

The suggested bracket notation would work, but it looks too similar to the competing array notation bar[]=. Since URLSearchParams standardizes on foo=a&foo=b and not bracket notation, I decided not to introduce brackets.

Note that we still need to write a standardized parser, and the parser can be more permissive with what it accepts. E.g., the serialize will set the base standard, but our parser can still choose to accept other formats within reason, and if they do not conflict with the serializer.

For example, we can unambiguously parse foo.bar=1 and foo[bar]=1 to { foo: { bar: 1 } } since the Seam API will not allow brackets or dots in param names.

t.is(
serializeUrlSearchParams({
foo: 1,
bar: { baz: [1, 'a'] },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does an array with a single element serialize to?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a test above, and info is in the README
image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was a leading question 😁 - this means we're losing type information - we're relying on our consumer knowing that foo is an array, whereas if we used the more explicit foo[]=1 we'll produce an array regardless of the cardinality

Copy link
Member Author

@razor-x razor-x Feb 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is covered in the README addition in this PR

Serialization is guaranteed to be well-defined within each type, i.e., if the type of value for a given key in the query string is fixed and known by the consumer parsing the string, it can be unambigously parsed back to the original primitive value.

The scope of this serializer is only in the context of supporting the Seam API. It does not need to preserve type information because the type information will be contained in the parsing step. The Seam API does not have "ambiguous" parameters, e.g., it does not allow for string | string[] or number | string so losing this type information is not a concern and can be moved out of scope.

Converting a JS object to a url will always be lossy unless explicitly encoded as JSON, so this library makes tradeoffs favoring readability and usability over less-lossy serializations, because it does not need to preserve the type information due to the constraints in the allowed schema domain.

Copy link
Member Author

@razor-x razor-x Feb 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@phpnode I am still trying to decide the best way to handle nullable params though. Currently null and undefined are both removed by the serializer, but a GET query for foo=null, i.e. "filter for when foo is null" vs. omitting foo, i.e. "no filter on foo" is a more common use case.

We could serialize { foo: null } to foo=null, but then the API cannot handle the literal string 'null' vs the value null. This is only an issue for nullable strings. Any other nullable param type (number, array, etc.) can handle foo=null as null would otherwise be an invalid value.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@phpnode I'm working on the parser scope here: seamapi/url-search-params-parser#1

For null, I'm leaning towards serializing it to null in all cases and allowing the parser to decide when to treat null as a string or a null. I don't think there are many valid cases where null would be a useful string value in a GET request.

This could be an option, e.g., serialzieNullToString, but I don't know when we would use a different value. The serializer should just make a decision on this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use an empty string to represent null - i think there's even fewer cases where we'd want to differentiate between an empty string and null, so you end up with ?foo=&bar=123 which seems fine. It's also how an empty form field would be serialized by the browser

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants