Repository navigation
v3.3: Allow in: query and in: querystring and/or multiple in: querystrings together? #5366
Description
IMPORTANT NOTE: @karenetheridge has convinced me that in: querystring and in: query really cannot be combined, so this original post here is not really accurate anymore. However,in: querystring overrides might work. I'll try to clean this all up or maybe re-file it when I get a chance.
Revised proposal starts at #5366 (comment)
NOTE: This is primarily relevant if #5320 is accepted, as it dramatically widens the scope of potential interactions by allowing global parameters. If #5320 is rejected, this can probably just be closed wontfix.
To keep things simple with in: querystring, we added two restrictions, which apply across both the Operation and Path Item level:
- There can only be one
in: querystringparameter - If there is an
in: querystringparameter, there cannot be anyin: queryparameters
We missed a querystring option elsewhere
However, we did overlook that the type: apiKey, in: query Security Scheme effectively adds an in: query parameter which we did not explicitly forbid (and I do not consider the current wording to implicitly forbid it, as "parameter" was intended to mean Parameter Object).
Technically, there isn't a problem here: You can just tack the API key parameter onto the query string on either end, and as long as you remove it first when parsing, there's no ambiguity.
None of the potential problems are new
Ambiguous groups of object-property-name-defined query paramters already occur within: query, explode: trueAs noted (and warned against) in Appendix E, with very particular use ofallowReserved: truewith minimal percent-encoding (and noform-urlencoded-specific escaping), plus use of aform-urlencodedparser, it is possible to misinterpret a+as an escaped space when it was serialized as a literal+. This requires the user to make an effort to work around the typical behavior, and we already warn that it will cause a bug if the user does so.
We can make the ambiguity better, and the escaping/encoding issue is not worse
We could also improve the situation with in: querystring by mandating its position relative to other query parameters (whether in: querystring or in: query). For example:
when multiplein: querystringparamters are present, the global ones MUST be serialized first (directly after the?), in the order they appear in the global array, then the path item ones, then the operation oneswhenin: querystringparamters are present, they MUST all appear before anyin: queryor security scheme parameters (or MUST all appear after, it doesn't matter as long as it is consistent)
This would substantially reduce the number of possible ways to parse the resulting URL when it is recieved.
We could also make corresponding SHOULD recommendations regarding in: query (and other) parameter ordering, we just can't make it a MUST because of compatibility. In fact, without this SHOULD, the behavior is already inherently implementation-defined.
(paging @karenetheridge for implementor feedback)
Activity
karenetheridge commentedon Jun 6, 2026
This is backwards compared to how things work now, with operation parameters overriding path-item parameters. Technically we could process them in any order, but the natural thing to do is to process operation parameters first, and keep track of the location+in tuple in a "seen" list, and then ignore the lower-priority parameter that matches an already-seen tuple. So to implement this, we'd need to iterate through all parameters at all levels first (operation first) to track of which ones we'll process, and then go back and process them in the proscribed other order.
I see that you're saying that if we want to allow multiple querystrings to be used together, then the order in which they're added must be fixed, so we can tell them apart when we deserialize. If we can pick any order we want, I'd suggest sticking with operation first for consistency and ease of implementation.
Once we're doing that, we can also lift the restriction of one querystring per endpoint entirely, which allows us to use two querystrings in a single location (operation, path-item or global).
I agree with forcing the appearance (and therefore the definition) of querystring parameters either ahead of or behind query parameters -- that's easy to implement when within a single definition location (operation, path-item, global), but the ordering will be mixed if parameters are coming from multiple definition location at once. I think this could quickly become impossible to parse correctly, especially when combined with parameters not declared
required: true, or with array or object query parameters where the number of tuples (the substring separated by&) is not known in advance.In fact, once I start considering examples, I think having multiple querystring parameters at once where not all of them are declared
required: trueis going to be problematic to parse:For the URI
https://example.com/foo?...some_encoded_content...&...other_encoded_content..., which parameter is alpha, which is beta, and which is gamma? There are only two, so one of these parameters is not provided. Which one?handrews commentedon Jun 6, 2026
@karenetheridge thanks for the detailed response!
I confess I was thinking about two passes- one to figure out what things got overridden, and then one to actually write the things into the query string. But I picked that ordering arbitrarily, and I agree that picking an ordering that allows for a single-pass processing would be better.
Do you meanin the
parametersarray? I was not actually mandating anything about that, and in fact, we cannot because it would be backwards-incompatible. I'm only talking about the order they get put in the URL.But are they more problematic than what we already have, which includes multiple
explode: trueobject query parameters where not all of them are declaredrequired: true?The goal here is to get the functionality can without making things worse. I think all of the multiple
in: querystringscenarios havein: query, explode: trueanalogues, which would suggest that none of the problematic cases are worse than what we already have. They are just shaped a bit differently (the schema is undercontentwithin: querystring, and underschemawithin: query, explode: true).karenetheridge commentedon Jun 6, 2026
Yes of course -- since the ordering is purely in the serializing/deserializing, it's invisible to the OAD and the user doesn't have to be aware of these ordering issues at all so there's no need to mandate it there; it's just a technicality that the implementation needs to follow to make sure things come out right.
How is this problematic at all? For serializing, all of the objects' keys and values are concatenated in the query string, and when deserializing, every parameter will get all the keys and values back identically.
It isn't all that sensible from a user perspective because you don't get back out exactly what you put in (you lose the information as to what properties went with which parameters, and there will be some duplication across the parameter values), but as far as the implementation goes it's perfectly fine, because they're all encoded the same way.
The querystring problem is different -- you can't correlate each querystring component with the right parameter, and if different media-types are used for each parameter, if you guess wrong you'll get decoding errors. It's not just that this is a nonsensical "that's legal but why would you do that" situation, but that it's actually impossible to implement deterministically. At least, I'm not seeing a way in which it could be done.
edited to add: I suppose we could still run into problems with the multiple query-as-object-explode-true scenario if the schemas for the different parameters were declared differently, in an incompatible way, so if a key-value pair ended up in a different parameter than what it started out in, a validation error could occur. But this is only a possibility... with multiple querystrings, it's a certainty unless all of the querystrings were declared as required: true. But maybe that's acceptable, as a tradeoff for getting this feature at all, even if it's limited? We'll have to call out these caveats very clearly in the OAS.
handrews commentedon Jun 6, 2026
@karenetheridge OK yeah you've convinced me: We really can't viably use
in: querystring's media type approach and have multiple of them. I was thinking too narrowly about when the media type isapplication/x-www-form-urlencoded, butin: querystringis intentionally broader than that. I'm actually a bit embarrassed to have lost sight of that!I think what this calls for is being even more clear that, if someone feels like they need more than one
form-urlencodedin: querystringparameter, then they need to think instead in terms ofin: query, explode: true.We can probably improve the wording to make it more intuitive that, since
in: querystringtreats the query string as a single "document" (and not a space in which to put... stuff), that it can't be combined with anything else at any level. Right now we don't really connect those concepts together.We probably do need to decide how API Key parameters work if
in: querystringis being used, but that's a headache for the security work, I think. Probably "implementation-defined if the querystring isform-urlencoded, and undefined otherwise" but idk.karenetheridge commentedon Jun 7, 2026
Re the clash with API Key, I think we can probably just add a mention that because this goes into the same space as a
queryparameter, the same exclusions apply (and any sort of OAD preprocessor that's not just a schema validation would be able to check for that, I think).handrews commentedon Jun 8, 2026
@karenetheridge One thing that occurrs to me is that we could treat
in: querystringat a lower level as overriding all instances of either type of query parameter at a higher level.In this example, there is a global tracking parameter. However, one special endpoint uses a JSONPath query which overwrites that global parameter (and any security scheme parameter). Within that endpoint, the POST operation creates the resource that is later queried, and (for... reasons...) takes that data in the query string:
I suppose we could also say that having even one
in: queryparameter at a lower level when the higher level isin: querystringoverwrites the entirein: querystringparameter. This lets you carve out exceptions, which is more important with globals. But you never mix things. You just switch back and forth from one to the other.I believe this avoids all parsing problems, and also allows single-pass processing as it is just a different (and arguably simpler) override rule compared to existing override rules.
handrews commentedon Jun 10, 2026
Thinking through what having some interactions might mean, first for just
in: querystringat multiple levels (noin: query).parametersentries are considered "the same" (causing a lower-level one to override a higher-level one) if bothinandnameare the samein: querystring, thenamefield does not appear in the serializationin: querystringfields are not allowed at the same level regardless ofnameI think the combination of these properties means that:
in: querystringentries with the same name at different levels produces the normal overriding behaviorin: querystringentries with different names at different levels is an error, for the following reasons:in: querystringentries would be retained because of their different namesin: querystringentries at the same level, which is an errorThis avoids creating any new rules for how parameters work. It also has the useful property that if, in a large and complex (and potentialy multi-document) OAD, you accidentally end up with different-
namein: querystringparameters at different levels, you get an error. This is desirable, as they were most likely named for different purposes.API designers who want to be able to override any
in: querystringwith any otherin: querystringcan just establish a convention to call them all the same thing. Since the name is not in the serialization, its only function is to determine whether twoin: querystringparameters result in an override or an error.handrews commentedon Jun 11, 2026
Next, let's consider
in: querystringat a lower level whenin: queryexists at a higher level. Conceptually, I think this makes sense becausein: querystringoccupies the entire query string, and therefore overrides all possiblenamevalues fromin: query. It is not necessary to correlate individul names even if thein: querystringmedia type isapplication/x-www-form-urlencoded, because thein: querystringmechanism is fundamentally different.This also makes sense, because if you have
in: query, name: foo, explode: true, schema: {type: object, ...}at two levels, the lower one completely replaces the higher one. You do not correlate by individual names in the object.So I think it's reasonable to allow replacement of all
in: queryat a higher level within: querystringat a lower level.handrews commentedon Jun 11, 2026
I forgot on the previous comment: One could apply the following argument I made for differently-named
in: querystringparameters at different levels to the "higherin: query, lowerin: querystring" case:The analogy being that we'd end up with "the same effect" as
in: queryandin: querystringat the same level, which ought to be an error.I suppose there is a logical inconsistency here that's worth thinking through.
handrews commentedon Jun 11, 2026
For completeness, let's look at
in: queryat a lower level whenin: querystringalready exists at a higher level.I don't think it's necessary to support this kind of override. It's less intuitive for a single
in: queryto override an entirein: querystring, and I'm quite certain we don't want to come up with some sort of partial override mechanism that would be entirely new in that it would have to look at schema property names. Let's not do that.If
in: querystringcan override itself, there are already other ways to alternate property combinations with$refs, the same as with any other schema.karenetheridge commentedon Jun 11, 2026
This is an error too, not an override.never mind! I wasn't awake.
handrews commentedon Jul 25, 2026
At this point, I think what makes the most sense is to add the global
parametersand not relax any of thequery/querystringrules. We can always relax them later, but my explorations of that idea above feel like adding a lot of complexity without being certain it's truly necessary. I'm going to post a PR that just adds the global field and we can start there.