【问题标题】:query parameter preserve as json查询参数保存为 json
【发布时间】:2025-12-15 17:40:02
【问题描述】:

我正在尝试以 JSON 格式存储 API 请求查询参数,以保留参数值的推断原始类型。我在事先不知道这些 API 长什么样的情况下这样做。 下面的代码一个一个地处理每个查询参数(由 & 分隔)。

    for (int i = 0; i < url_arg_cnt; i++) {
        const http_arg_t *arg = http_get_arg(http_info, i);
        if (cJSON_GetObjectItem(query, arg->name.p) == NULL) {
            // Currently just treating as a string.
            cJSON_AddItemToObject(query, arg->name.p, cJSON_CreateString(arg->value.p));
            SLOG_INFO("name:value is %s:%s\n", arg->name.p, arg->value.p);
        } else {
            //duplicate key.
        }

上面的代码,用于输入

?start=0&count=2&format=policyid|second&id%5Bkey1%5D=1&id[key2]=2&object=%7Bone:1,two:2%7D&nested[][foo]=1&nested[][bar]=2

我得到了这些照片:

name:value is start:0
name:value is count:2
name:value is format:policyid|second
name:value is id[key1]:1
name:value is id[key2]:2
name:value is object:{one:1, two:2}
name:value is nested[][foo]:1
name:value is nested[][bar]:2

根据这个文件和我研究过的其他地方, https://swagger.io/docs/specification/serialization/

对于如何传递查询参数没有达成共识,因此不能保证我在这里会遇到什么。所以我的目标是支持尽可能多的变化。 这些可能性似乎是最常见的:

数组:

?x = 1,2,3

?x=1&x=2&x=3

?x=1%202%203

?x=1|2|3

?x[]=1&x[]=2

字符串:

?x=1

对象,可以嵌套:

?x[key1]=1&x[key2]=2

?x=%7Bkey1:1,key2:2%7D

?x[][foo]=1&x[][bar]=2

?fields[articles]=title,body&fields[people]=name

?x[0][foo]=bar&x[1][bar]=baz

任何想法如何最好地做到这一点?基本上对于这些查询参数,我想聚合('exploded')属于一起的参数并保存到 query 适当的预期 json 对象。有问题的行:

cJSON_AddItemToObject(query, arg->name.p, cJSON_CreateString(arg->value.p));

【问题讨论】:

  • 您需要三个大括号 - 双倍以在输出中获得实际的 {,再加上单倍以执行实际的变量替换。
  • 我不明白你在问什么,或者说你希望从答案中学到什么不同。似乎您想出解析一个版本。您是否在问如何构造代码以解析不同的选项?您是否尝试将每个(数组、字符串、对象)标准化为标准类型(即数据建模问题)?您是否尝试存储解析的值以便恢复原始字符串?等等

标签: c json url rest cjson


【解决方案1】:

将 URI 查询转换为 JSON

这篇文章将提供更通用(规范)的方法来解决从 URI 字符串中提取变量的问题。

查询是跨多个描述性标准(RFC 和规范)定义的,因此如果采用规范方法,我们需要使用规范创建查询的规范化形式,然后才能构建对象。

TL;DR

为了确保我们能够实现规范并能够满足未来扩展的需求,将查询转换为 JSON 的算法应该分步进行,每一步逐步构建查询的规范化形式,然后才能实现转换为 JSON 对象。为此,我们需要以下步骤:

  • 从 URI 中提取查询
  • 拆分为key=value
  • 规范化key(构建对象层次结构)
  • 规范化value(填充对象属性并构建属性数组)
  • 基于规范化的key=value 构建 JSON 对象

这样的步骤分离将允许更容易地采用规范中的未来更改。可以使用 RegEx 或解析器(BNF、PEG 等)对值进行解析。

转换步骤

  1. 首先要做的是从 URI 中提取查询字符串。这在RFC3986 中进行了描述,并将在其自己的提取查询字符串部分中进行解释。正如我们稍后将看到的,查询段的提取可以通过 RegEx 轻松完成。

  2. 从URI中提取查询字符串后,需要对查询所传达的信息进行解释。正如我们将在下面看到的,查询在RFC3986 中有一个非常松散的定义,查询传递变量的情况在RFC6570 中进一步阐述。在提取过程中,算法应该提取值(以key=value 的形式)并将它们存储在映射结构中(一种方法是使用严格,如下面的SO post 所述。解释查询字符串提供了该过程的概述。

  3. 将变量分离并以key=value的形式放置后,下一阶段是对key进行规范化。对key 的正确解释将允许我们从key=value 结构构建JSON 对象的层次结构。 RFC6570 没有提供太多关于如何规范化key 的信息,但是OpenAPI specification 提供了如何处理不同类型的key 的良好信息。规范化将在规范化密钥

    部分中进一步阐述
  4. 接下来我们需要通过继续构建RFC6570 来规范化变量,该RFC6570 定义了多个级别的变量类型。这将在规范化值

    部分中进一步阐述
  5. 最后阶段是使用cJSON_AddItemToObject(query, name, cJSON_CreateString(value)); 构建 JSON 对象。更多细节将在构建 JSON 对象部分讨论。

在实施过程中,可以将部分步骤合并为一个步骤以优化实施。

提取查询字符串

RFC3986 是管理 URI 的主要描述性标准,它将 URI 定义为:

URI         = scheme ":" hier-part [ "?" query ] [ "#" fragment ]

query 部分在 RFC 的第 3.4 节中定义为 URI 的段,例如:

...查询组件由第一个问题表示 标记 ("?") 字符并以数字符号 ("#") 字符结尾 或在 URI 的末尾。 ...

query 段的正式语法定义为:

query         = *( pchar / "/" / "?" )
pchar         = unreserved / pct-encoded / sub-delims / ":" / "@"
unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"
pct-encoded   = "%" HEXDIG HEXDIG
sub-delims    = "!" / "$" / "&" / "'" / "(" / ")"
                 / "*" / "+" / "," / ";" / "="

这意味着在遇到# 之前,query 可以包含更多?/ 的实例。实际上,只要?第一次出现之后的字符在没有特殊含义的字符集中,那么直到遇到第一个#之前找到的都是query

同时,这也意味着子分隔符&amp;,以及?在查询字符串中遇到时根据本RFC没有特殊含义,只要它在正确的URI 中的表格和位置。这意味着每个实现都可以定义自己的结构。第 3.4 章中的 RFC 语言通过使用 often 而不是 always 为其他解释留出空间来确认这些含义

... 但是,作为查询组件 通常用于携带识别信息的形式 “key=value”对...

此外,RFC 还提供了以下 RegEx,可用于从 URI 中提取查询部分:

regex   : ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
segments:   12            3  4          5       6  7        8 9

捕获#7 是来自 URI 的查询。

如果我们对 URI 的其余部分不感兴趣,提取查询的最简单方法是使用 RegEx 拆分 URI 并提取不包含前导 ? 或终止#

此 RFC3986 进一步扩展为 RFC3987 以涵盖国际字符,但 RFC3986 定义的 RegEx 仍然有效

从查询字符串中提取变量

要将查询字符串分解为key=value 对,我们需要对RFC6570 进行逆向工程,这为变量的扩展和构造有效的query 建立了描述标准。正如 RFC 所述

... URI 模板提供了 URI 空间的结构描述 并且,当提供变量值时,机器可读的指令 关于如何构造与这些值对应的 URI。 ...

从 RFC 中,我们可以为查询中的变量提取以下语法:

query         =  variable *( "&" variable )
variable      =  varname "=" varvalue

varvalue      = *( valchar / "[" / "] / "{" / "}" / "?" )

varname       =  varchar *( ["."] varchar )
varchar       =  ALPHA / DIGIT / "_" / pct-encoded

pct-encoded   = "%" HEXDIG HEXDIG
valchar       = unreserved / pct-encoded / vsub-delims / ":" / "@"
unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"
vsub-delims   = "!" / "$" / "'" / "(" / ")"
                 / "*" / "+" / ","

可以使用实现上述语法的解析器执行提取,或者使用以下正则表达式迭代查询并提取 (key, value) 对。

([\&](([^\&]*)\=([^\&]*)))

如果我们使用 RegEx,请注意在上一节中我们省略了“?”在查询的开头和结尾的“#”,所以我们不需要在变量的分隔中处理这些字符。

规范化密钥

描述性标准RFC6570 提供了密钥格式的通用规则,当涉及到构造对象时的密钥解释规则时,RFC 并没有多大帮助。一些规范,如OpenAPI specificationJSON API Specification) 等可以帮助解释,但它们没有提供完整的规则集,而是一个子集。为了使事情顺利进行,一些 SDK(例如 PHP SDK)有自己的构建密钥的规则。

在这种情况下,最好的方法是为密钥规范化创建分层规则,将密钥转换为统一格式,类似于json path dot notation。分层规则将允许我们控制模棱两可的情况(在规范之间发生冲突的情况下),但控制规则的顺序。 json 路径表示法将允许我们在最后一步中构建对象,而无需确保 key=value 对的正确顺序。

以下是规范化格式的语法:

key      = sub-key *("." sub-key )
sub-key  = name [ ("[" index "]") ]
name     = *( varchar )
index    = NONZERO-DIGIT *( DIGIT )

此语法将允许使用 foofoo.bazfoo[0].bazfoo.baz[0]foo.bar.baz 等键。

以下是设置规则和转换的良好起点

  1. 平键 (key -> key)
  2. 属性键(key.atr -> key.atr
  3. 数组键(key[] -> key[0]
  4. 对象数组键 (key[attribute] -> key.attribute), (key[][attribute] -> key[0].attribute), (key[attribute][] -> key.attribute[0])

可以添加更多规则来解决特殊情况。在转换过程中,算法应该从最具体的规则(底部规则)传递到最通用的规则,并尝试找到完全匹配。如果找到完全匹配,则键将被正常形式覆盖,其余规则将被跳过。

标准化值

与键的规范化类似,在值表示列表的情况下,值也应该进行规范化。我们需要将值从任意列表格式转换为由以下语法定义的form 格式(逗号分隔列表):

value        = singe-value *( "," singe-value ) 
singe-value  = *( unreserved / pct-encoded )

此语法将允许我们将值采用aa,ba,b,c 等形式。

从值字符串中提取值列表可以通过用有效的分隔符(“,”,“;”,“|”等)分割字符串并以规范化形式生成列表来完成。

构建 JSON 对象

一旦键和值被规范化,将平面列表(映射结构)转换为 JSON 对象可以通过单次遍历列表中的所有键来完成。键的规范化格式会对我们有所帮助,因为键传达了对象中关于他的层次结构的全部信息,所以即使我们没有遇到一些中间属性,我们也能够构建对象。

类似地,我们可以从变量本身识别属性的值应该是平面字符串还是数组,因此在这里也不需要额外的信息来创建正确的表示。

替代方法

作为替代方法,我们可以构建一个完整的语法来创建 AST(抽象语法树),并使用该树来生成 JSON 对象,但是由于格式的多种变化以及未来扩展的能力,这种方法不太灵活。

有用的链接

【讨论】:

    【解决方案2】:

    我最近遇到了同样的问题,并将分享从这一集中获得的一些智慧。 我假设您正在 MITM 设备(网络防火墙等)上实现此功能。 正如问题中指出的那样,查询参数的传递方式没有达成共识。没有一个标准或一组规则来管理这一点——事实上,任何服务器都可以实现自己的语法,只要服务器代码支持该语法。最好的方法是 1) 决定支持哪些查询参数形式(尽你所能,可能尽可能多)和 2) 仅支持这些形式,将其余形式(不支持的形式)视为字符串值,例如您当前的代码可以。

    不值得过多担心相关类型的保存/推断的准确性,或者将其形式化/概括为重量级解决方案,因为 1)您可能遇到的语法任意性(不一定符合任何标准, web 服务器真的可以做任何他们想做的事,因此查询参数通常不符合,比如说,引用的 swagger 标准)和 2)查看查询参数只会给你这么多信息——实施的好处/价值除了模糊的近似值(如前所述,根据您自己定义的规则)之外的任何东西都很难被看到。想想即使是最简单的情况,它们是多么模糊:你必须假装在 x=something&x=something 爆炸的情况下,数组必须至少有两个元素。如果只有一个元素——x=something——你把它当作一个字符串,你怎么知道它是一个数组还是一个字符串? x=1 情况如何,1 是字符串还是数字,原始/预期类型?另外, x=foo&y=1 | 怎么样? 2 | 3?或者当您看到带有空格的“1、2、3”时?是否应该忽略空格,它们是数组分隔符本身,还是它们实际上是数组元素的一部分。最后,你怎么知道预期的字符串不是“1 | 2 | 3”本身,这意味着它不是一个数组!

    因此,在解析这些字符串并尝试支持/推断所有这些变体(不同的规则)时,最好的方法是定义自己的规则(可以/满意的规则)并仅支持这些规则。

    【讨论】: