H
Honoโ€ข4w ago
Paul

How to accept flattened objects in query params?

It seems that the hono client calls String() on the query params so that any objects passed through which should normally be something like filters[key]=value ends up becoming filters="[object Object]". 1. I'm pretty sure this use-case should be supported since it appears out in the wild when using query params 2. Is there a way to make this work? or is the recommendation to go with flat query objects / honojs client doesn't support objects in query params? I think this is an error/bug that should be updated. Just want to confirm here before writing an issue on github...
// dev.spec.ts
it.only("GET /dev/query returns nested query params", async () => {
const response = await testApiClient.api.dev.query.$get({
query: { filters: { active: true } },
});
expect(response.status).toBe(HttpStatusCodes.OK);
});
// dev.spec.ts
it.only("GET /dev/query returns nested query params", async () => {
const response = await testApiClient.api.dev.query.$get({
query: { filters: { active: true } },
});
expect(response.status).toBe(HttpStatusCodes.OK);
});
// dev.routes.ts
import { createRoute, z } from "@hono/zod-openapi";

export const getQuery = createRoute({
method: "get",
path: "/dev/query",
operationId: "getQuery",
tags,
request: {
query: z.object({
filters: z.any().optional(),
}),
},
responses: {
[HttpStatusCodes.OK]: jsonContent(
z.object({
timestamp: z.string().datetime(),
success: z.any(),
}),
"Successfully retrieved query"
),
},
});
// dev.routes.ts
import { createRoute, z } from "@hono/zod-openapi";

export const getQuery = createRoute({
method: "get",
path: "/dev/query",
operationId: "getQuery",
tags,
request: {
query: z.object({
filters: z.any().optional(),
}),
},
responses: {
[HttpStatusCodes.OK]: jsonContent(
z.object({
timestamp: z.string().datetime(),
success: z.any(),
}),
"Successfully retrieved query"
),
},
});
[1/2]
22 Replies
Paul
PaulOPโ€ข4w ago
// dev.handler.ts
export const getQuery: PublicRouteHandler<typeof routes.getQuery> = async c => {
const { filters } = c.req.valid("query");

console.log({ filters });
// {filters: [object Object] }

const url = c.req.url;
console.log({ url });

// const parsedFilters = JSON.parse(filters);
// console.log({ parsedFilters });
// This leads to an error because [object Object] is not a valid JSON string

return c.json(
{
success: true,
timestamp: new Date().toISOString(),
},
200
);
};
// dev.handler.ts
export const getQuery: PublicRouteHandler<typeof routes.getQuery> = async c => {
const { filters } = c.req.valid("query");

console.log({ filters });
// {filters: [object Object] }

const url = c.req.url;
console.log({ url });

// const parsedFilters = JSON.parse(filters);
// console.log({ parsedFilters });
// This leads to an error because [object Object] is not a valid JSON string

return c.json(
{
success: true,
timestamp: new Date().toISOString(),
},
200
);
};
Console output:
{ filters: '[object Object]' }
{ url: 'http://localhost/api/dev/query?filters=%5Bobject+Object%5D' }
{ filters: '[object Object]' }
{ url: 'http://localhost/api/dev/query?filters=%5Bobject+Object%5D' }
[2/2]
Arjix
Arjixโ€ข4w ago
you could make middleware that adds support for it but it is indeed unfortunate that this happens usually for search params, you don't do item[]=1&item[]=2 but item=1&item=2 at least that's how it is in spring boot for example the item[]=1 syntax is for forms
Arjix
Arjixโ€ข4w ago
yep, looks like what I sent works
No description
Arjix
Arjixโ€ข4w ago
it does feel intuitive to do what forms do, but for some reason there is a difference, and I don't know if there is a specification for this or if everybody copied each other's implementation this aligns more with how Headers behave, so I'd say it's one way of remembering it you should never construct the query string yourself though, since there is a built-in class for that URLSearchParams
Arjix
Arjixโ€ข4w ago
No description
Arjix
Arjixโ€ข4w ago
oh you were not talking about arrays, but objects? not even forms have objects
Paul
PaulOPโ€ข4w ago
Thanks for your responses. To clarify, the item and filters example you're giving look like the ability to accept arrays where a zod schema would be query: z.object({ item: z.string().array() }); But for objects like..
query: z.object({
filters: z.object({
active: z.boolean(),
name: z.string(),
})
});
query: z.object({
filters: z.object({
active: z.boolean(),
name: z.string(),
})
});
It should come out like this... filters[active]=true&filters[name]=Sally I'm using the hono testApiClient and it seems to call String() on all query params, such that even middleware doesn't work since all query params appear to be stringified as opposed to serialized and thus in any middleware, when looking for the query property, I get [object Object] I understand I can make my query params flat, but in my conversations with Google Gemini, it seems to suggest this is a conventional method of accepting query params that should work
Paul
PaulOPโ€ข4w ago
Yeah was talking about nested objects, not arrays. Sorry if I wasn't clear at first. There's an existing issue that was closed here about the same thing: https://github.com/honojs/middleware/issues/737
GitHub
zValidator parsing url queries containing nested object ยท Issue #7...
Hi guys, Wondering how to validate (and design schema) a route with zValidator with an url like this : http://hostname/products/paginate?limit=8&amp;filters[productName]=fr. My issue is filters whi...
Arjix
Arjixโ€ข4w ago
I've never seen a framework/library support such things out of the box maybe laravel? can't remember
Paul
PaulOPโ€ข4w ago
I see...
Arjix
Arjixโ€ข4w ago
you could just make ur own middleware tho zValidator is nothing special zod does the heavy lifting and the built-in URLSearchParams class doesn't allow such structures, so it's probably out of spec, and hono follows the specs, so it's probably not going to get added to hono
Paul
PaulOPโ€ข4w ago
In the context of writing my own validator, since I'm using hono zod-openapi, the createRoute function doesn't accept a zValidator function right? So I assume that means the schema would need to be z.string().transform(...) to transform a string into an object but then I'd lose the openapi definitions. I see. Okay I can understand that. It's helpful that at least that's clear. It looks like going flat is the way to go here then Appreciate your help on this Arjix
Arjix
Arjixโ€ข4w ago
uhh you could keep both and only use ur custom middleware
Paul
PaulOPโ€ข4w ago
The middleware approach doesn't work because it gets stringified to "[object Object]" at least when I use the hono test client such that it can't be JSON.parse() 'ed back
Arjix
Arjixโ€ข4w ago
you do ctx.req.valid('query'), if you simply don't do that, then you are not invoking the zValidator but you still have the openapi types as long as you re-use the same zod schema, it should be fine (having one source of truth) I may be forgetting smth uhh, I don't really use zod-openapi, so forgive me if I am wrong I mainly use hono-openapi
Paul
PaulOPโ€ข4w ago
I'm also using hono-openapi. I think part of my issue is with the hono test client and possibly the honoClient itself import { testClient } from "hono/testing"; When using it, when I pass a nested object in query params, it becomes a literal string value of "[object Object]" such that middleware receives a string of "[object Object]" so middleware doesn't work when using it with the client because the client serialization is equivalent to calling String() So even if I don't validate the data, the data can't be passed through properly when using the hono client. Requesting the url directly with fetch() or app.request() would work as in your picture. In any case, I guess flat query params is the approach to use. Not sure if the hono (test) client should be updated based on the above notes...
Arjix
Arjixโ€ข4w ago
you could make a utility function to convert an object to a flat query I believe hono won't mess with the params if you directly pass a URLSearchParams instance
Paul
PaulOPโ€ข4w ago
Yeah. I could JSON.stringify() the object passed into filter and that would work. I think with all these special cases required by the caller to do special parsing when using the client, I'll just revert to flat structures. Appreciate your help again
Arjix
Arjixโ€ข4w ago
Yeah. I could JSON.stringify() the object passed into filter and that would work.
that's not what I suggested, and I am unsure if you intented to say "I could also ..."
Paul
PaulOPโ€ข4w ago
Ah. You said utility function so I was thinking the utility function would use JSON.stringify()
Arjix
Arjixโ€ข4w ago
nah, it would recursively iterate over all keys and flatten the object overkill, but that's basically what I did in PHP ๐Ÿ’€
Paul
PaulOPโ€ข4w ago
But yeah overall, I'm okay to change it to just be flat to avoid the caller needing to handle these special cases I see

Did you find this page helpful?