在 InterSystems IRIS 中通过图形化方式处理 JSON:互操作性——JSON 虚拟文档
在 InterSystems IRIS 中,用户可以轻松地通过动态对象(Dynamic Object)操作 JSON。但动态对象没有模式,因此 DTL 图形化编辑器中没有可拖动的字段。 本文介绍了一种将 JSON 消息转换为虚拟文档(下文简称 JVDoc)的实现方案:其模式来源于 JSON Schema 或示例 JSON,DTL 编辑器、规则编辑器、搜索表和验证功能均可直接使用该虚拟文档,而消息本身仍以接收时的原始形式存储,并在系统中无损通过。 文末提供了可直接运行的测试用例。
问题
在互操作性场景中处理 JSON 十分常见,动态对象虽然方便,但它们没有模式,这会带来三个困扰:
- DTL 编辑器不显示字段树,因此无法通过拖拽创建映射,而必须手动编写代码。
- 在规则编辑器中输入
Document.{时,不会提供字段建议。 - 消息查看器无法按消息内的字段进行搜索。
通常的做法是使用 %JSON.Adaptor 生成类。这虽然解决了图形化映射的问题,但有两个代价:每个消息都必须先作为对象实例化,这对大型消息来说开销很大;此外,导入时,模式中不存在的字段会被丢弃,因此无法实现无损直通。
该思路
IRIS 虚拟文档框架(Ens.VDoc.Interface)专为没有对应类的结构化文档而设计:编辑器通过该框架获取字段树,并在运行时通过路径读写值。JVDoc 基于该接口实现了 JSON:
- 原始的 JSON 流会被存储起来,仅在首次访问路径时才会被解析为动态对象。
- 已修改的消息在保存时会重新序列化;未修改的消息则保持字节级完全一致。
- 文档类型(DocType)写为
Category:Name,并对应一个字段树;编辑器、规则、搜索表和验证功能均读取同一棵树。 - 路径的格式为
a.b(2).c,数组写为name(),例如items().sku。
主要功能
1. 根据 JSON 模式或示例 JSON 生成 DocType
字段树可来自以下三种来源:
- JSON 模式:文档内
$ref,支持 allOf 合并、oneOf/anyOf(属性的并集)以及终止递归,同时也支持 $anchor 引用、if/then/else 以及 minLength、maxLength、exclusiveMinimum、exclusiveMaximum、multipleOf、 minLength、maxLength、minItems、maxItems、uniqueItems、minProperties、maxProperties、dependentRequired、additionalProperties、not、contains、minContains、maxContains、dependentSchemas、propertyNames 和 const 约束。适用于用户可提供 JSON 模式的情况。 - 一个或多个示例 JSON:字段进行并集处理;整数和十进制数合并为数字;当不同示例中字段的类型不一致时,以多数为准;字段是否为必填项取决于其是否出现在所有示例中。适用于没有 JSON 模式但有示例 JSON 的情况。
- 消息存储中已有的消息:仅记录结构,绝不记录任何字段值。适用于没有 JSON 模式,但互操作性生产环境中已包含 JSON 消息的情况。
每次导入都会报告警告(跳过的属性名称、无法确定类型的字段、截断的递归等),并且字段树可以再次导出为 draft-07、2019-09(draft-08)或 2020-12 版本的 JSON Schema。
应调用哪种方法取决于您拥有的数据以及它描述的消息类型数量:
| **您拥有** | **调用** | **您将获得** | **注** |
|---|---|---|---|
| 一个以消息本身为根节点的 JSON 模式 | `SchemaImport.FromFile(cat, name, path)` 或 `FromJSONSchema(cat, name, schema)` | 一个文档类型 | 通常情况:每种消息类型对应一个模式文件。文件内的 `$ref` 至 `$defs`/`definitions` 会被解析。 |
| 一个描述多个模型的 JSON 模式文件 | `SchemaImport.FromBundleFile(cat, path)` 或 `FromBundle(cat, schema)` | 每个模型一个 DocType | 默认源为 `$defs`(或 `definitions`),每个 DocType 以其键命名。 若将 `"properties"` 作为第三个参数传递,则采用根节点的属性;若传递以逗号分隔的名称列表,则仅导入其中部分属性;若传递根节点名称,则将根节点也作为 DocType 导入。模型之间可以 `$ref` 相互引用。 |
| 一条示例消息 | `SchemaInfer.FromSample(cat, name, json)` | 一个 DocType,所有字段均为可选 | 对于编辑器而言已足够;仅凭一个示例,无法将任何内容称为必填项。 |
| 几个示例消息 | `SchemaInfer.FromSamples(cat, name, array, , required)` 或 `FromSampleFile(cat, name, path, multi)` | 一种文档类型 | 字段采用联合类型,类型由多数决定;当 `required`=1 时,只有当每个示例都包含该字段时,该字段才被视为必填。 |
| 消息存储中已有的消息 | `SchemaInfer.FromStoredDocs(cat, name, filterDocType, max)` | 一种文档类型 | 读取最新的 `max` 消息,可选指定一种文档类型;仅记录结构信息。 |
经验法则:如果文件的根节点是消息,请使用 FromFile;如果文件是模型目录,请使用 FromBundle。将 FromFile 指向目录会生成一个 DocType,其字段为模型名称,这通常并非您所期望的结果。
2. DTL 中的图形映射
选择 JVDoc.Document 作为源类和目标类,然后选择 DocType,即可像处理 HL7 一样拖动字段。以下是几个细节:
-
源侧和目标侧均包含
()的映射(例如items().sku到lines().code)会自动按元素逐个展开。 -
DTL 传递的值均为字符串;写入时会根据字段树中的类型转换为数字、布尔值或 null。 目标路径若不在字段树中,则按以下规则处理:JSON 对象或数组文本将转换为子树,其他文本将作为字符串写入,而 JVDoc 源数据将连同其真实的 JSON 类型一并复制。
-
create='copy'保留未映射的字段,包括模式之外的字段;使用create='new'时,目标仅包含已映射的字段。
3. 无损直通
消息将作为原始 JSON 流进行存储。 仅被读取且从未被修改的消息,在经过 Production 处理、保存并再次打开后,字节级完全一致,包括空格、数字格式以及模式外的字段。由 DTL 修改的文档会在保存时被重新序列化一次;克隆的文档及其源文档互不影响。
4. 路由规则
在路由引擎的规则中,请分别填写约束条件:消息类 JVDoc.Document,以及与冒号两侧 DocType 两部分相匹配的文档类别和文档名称。 在条件中输入 Document.{ 会打开字段列表,您可以在其中输入 Document.{category.name}="Dogs"。

5. 消息查看器和搜索表
在“消息查看器”中,有两种方法可以根据 JSON 内容查找消息。
VDoc 属性路径:无需任何设置。
在“扩展条件”中添加一个类型为“VDoc 属性路径”的条件:
类:JVDoc.Document
文档类型:目标虚拟文档类型,例如 PetStore:pet
属性路径:目标属性,例如 tags().name;您也可以仅输入 {category.name} 来搜索任何文档类型的消息。然后指定一个条件,例如 = tag3。
()匹配数组中的任意元素:tags().name = tag3查找至少有一个标签名为tag3的消息。- 匹配为精确匹配且区分大小写。不包含该字段的消息将不被匹配。
- 系统只会检查具有 DocType 或条件中指定的 DocType 的消息。
- 消息查看器会打开每条候选消息以读取其值,因此当消息量较大时,请通过时间或标题条件缩小搜索范围,或使用搜索表。

搜索表:用于您经常搜索的字段。
VDoc 属性路径虽然方便,但它必须打开每条候选消息,因此在处理非常大的数据量时会变慢。搜索表会在保存消息时构建索引,搜索时会遍历该索引,因此无需打开消息。
搜索表使用产品自身的机制。路径需用方括号括起,而 () 表示遍历数组,例如 [tags().name](类定义见下文案例 6)。 索引会在保存新文档时自动构建,当已保存文档的内容或文档类型(DocType)发生变化时会重新构建(为避免重复,会先删除旧行);删除或清除一条消息会移除其对应的行。之后,在“消息查看器”的“扩展条件”中选择“SearchTable 字段”,即可按字段过滤消息。
注意:查询值无需加引号,且匹配不区分大小写。

**该使用哪一种?**两者的区别在于处理时机:搜索表会在每条消息保存时对其建立索引,而 VDoc 属性路径搜索则会在您搜索的瞬间读取所有候选消息。
| **搜索表** | **VDoc 属性路径** | |
|---|---|---|
| 设置 | 一个搜索表类,通过 `SetSearchTable` 与 DocType 绑定 | 无 |
| 运行原理 | 当消息被保存时,相关值会被写入搜索表;搜索操作会在 SQL 中对该表进行连接 | 在搜索时,会逐一打开并解析候选消息 |
| 速度 | 速度快,即使处理大量消息也是如此 | 消息数量过多时速度会变慢;可通过时间范围或标题条件来缩小范围 |
| 涵盖的消息 | 仅涵盖绑定建立后保存的消息;若需获取更早的消息,请运行 `JVDoc.Document.Reindex` | 所有消息及其当前内容 |
| 字段 | 仅包含在 `SearchSpec` 中声明的字段 | 任何路径,在搜索时选择 |
| 匹配 | 不区分大小写 | 精确匹配且区分大小写 |
| 长值 | 截断至 200 个字符 | 进行完整比较 |
| 对象和数组 | 不按索引比较 | 作为 JSON 文本进行比较 |
| 可在其他地方使用 | 普通 SQL 表:SQL 查询、报表、代码 | 仅限消息查看器 |
经验法则:针对用户经常搜索的字段(如订单号、患者ID)定义一个搜索表;对于偶尔进行的搜索、故障排除以及无人计划搜索的字段,则使用 VDoc 属性路径。
6. 根据模式进行验证
Validate(实例方法)和 ValidateJSON(类方法)在 JVDoc.Document 上接受一个由开关字母组成的 pValidationSpec 参数,用于选择验证规则:
r:缺少必填字段和依赖必填字段t:类型不匹配e:枚举p:模式(正则表达式)b:范围、倍数、长度、项目数量、唯一性及属性数量(minimum、maximum、exclusiveMinimum、exclusiveMaximum、multipleOf、minLength、maxLength、minItems、maxItems、uniqueItems、minProperties、maxProperties)c:consta:additionalPropertiess: not、contains 和 propertyNameso:oneOf 和 anyOfu:模式外的字段(默认关闭,因为直通是前提)
all 等于 rtepbcaso,而 - 前缀会禁用该字母(例如 all-p)。 错误按路径列出,例如 items(1).qty: expected integer, got string。
在路由入口处,您可以使用 JVDoc.ValidatingRouter:将 Validation 设置为 dmall,这样验证失败的消息将不再被路由; Act On Validation Error 决定是报错还是静默丢弃,而 Bad Message Handler 可以将错误消息转发到您指定的目标。
测试用例
创建测试容器:
git clone https://github.com/ISCQP/JSONVDoc.git
cd <repository directory>
docker compose up -d
容器内的演示环境已就绪,互操作性生产环境已启动:
- SMP 网址:http://127.0.0.1:55773/csp/sys/Utilhome.csp
- 用户名/密码:superuser / SYS
- 命名空间:TESTBED
案例 1:根据多个样本推断模式
Set smps = [{"id":1,"price":10,"status":"new","meta":{"a":1}},{"id":2,"price":10.5,"status":"paid","extra":"x"},{"id":"3","price":null,"status":"new","meta":null}]
Set sc = ##class(JVDoc.SchemaInfer).FromSamples("Demo", "Merged", smps, , 1)
Do ##class(JVDoc.SchemaImport).Outline("Demo:Merged")
Do ##class(JVDoc.SchemaImport).Report()
id : integer
price : number
status : string
meta : object ?
a : integer
extra : string ?
WARN: mixed types at 'id': numeric(2), string(1); chose numeric
1 message(s)
? 表示可选。在 price 中,整数和小数合并为数字类型,而 null 不影响类型; id 在一个样本中是字符串,因此根据多数样本判定为整数并发出警告;meta 和 extra 并非出现在所有样本中,因此它们是可选的。
情况 2:导入 JSON 模式并进行验证
首先导入 Order 的模式,然后定义一个简单的验证方法:
Set sch = {"type":"object","required":["order_id"],"properties":{"order_id":{"type":"string"},"total":{"type":"number"},"paid":{"type":"boolean"},"items":{"type":"array","items":{"type":"object","properties":{"sku":{"type":"string"},"qty":{"type":"integer"}}}}}}
Write ##class(JVDoc.SchemaImport).FromJSONSchema("Demo", "Order", sch), !
Class Demo.Test
{
ClassMethod Check(json As %String, spec As %String)
{
Set d = ##class(JVDoc.Document).%New(json)
Set d.DocType = "Demo:Order"
Set sc = d.Validate(spec)
Write $Select($System.Status.IsOK(sc):"OK", 1:$System.Status.GetErrorText(sc)), !
}
}
Do ##class(Demo.Test).Check("{""order_id"":""A1"",""total"":12.5}", "all")
Do ##class(Demo.Test).Check("{""total"":12.5}", "all")
Do ##class(Demo.Test).Check("{""order_id"":""A1"",""total"":""x"",""items"":[{""sku"":""S1"",""qty"":""2""},{""sku"":""S2"",""qty"":2.5}]}", "all")
Do ##class(Demo.Test).Check("{""order_id"":""A1"",""extra"":{""keep"":1}}", "u")
OK
ERROR #5001: order_id: required field is missing
ERROR #5001: total: expected number, got string
ERROR #5001: items(1).qty: expected integer, got string
ERROR #5001: items(2).qty: expected integer, got number
ERROR #5001: extra: unknown field
第三次调用一次报告了三个类型错误;第四次调用仅触发了 u,因此只报告了 extra,即一个超出模式范围的字段。
情况 3:一个模式文件包含多个 DocType
测试容器包含位于 /opt/jvdoc/src/Demo/petstore.json 的 PetStore 模型文件。 其根节点是一个信封:根节点的 properties 包含三个条目—— pet、 order 和 user——每个条目都包含一个指向 definitions 中模型的 $ref(该位置包含 ApiResponse、----185-----、Pet、Tag、Order 和 User):
{
"$schema": "http://json-schema.org",
"title": "PetstoreModels",
"type": "object",
"properties": {
"pet": {"$ref": "#/definitions/Pet"},
"order": {"$ref": "#/definitions/Order"},
"user": {"$ref": "#/definitions/User"}
},
"definitions": {
"Pet": {"type": "object", "required": ["name", "photoUrls"], "properties": {"id": {"type": "integer"}, "category": {"$ref": "#/definitions/Category"}, "name": {"type": "string"}, "photoUrls": {"type": "array", "items": {"type": "string"}}, "tags": {"type": "array", "items": {"$ref": "#/definitions/Tag"}}, "status": {"type": "string", "enum": ["available", "pending", "sold"]}}},
"...": "Category, Tag, Order, User, ApiResponse ..."
}
}
根据根属性导入模式文件,并在 PetStore 类别中生成文档类型:
Do ##class(JVDoc.SchemaImport).FromBundleFile("PetStore", "/opt/jvdoc/src/Demo/petstore.json", "properties")
FromBundleFile 参数:
- 第一
"PetStore":类别;所有生成的文档类型都将归入此名称下。 - 第二个:模式文件的服务器端路径,以 UTF-8 格式读取。
- 第三
"properties":子模式的来源。"properties"表示根节点的每个属性将生成一个 DocType,其名称取自属性键;默认情况下"defs"则会从$defs(若不存在则回退到definitions)中获取每个条目。 - 可选的第四项:用逗号分隔的名称列表,用于仅导入其中部分(例如
"pet,order");可选的第五项:非空值将根节点本身作为该名称的 DocType 导入;可选的第六项:最大递归深度(默认值为 8)。
1
INFO: created PetStore:pet
INFO: created PetStore:order
INFO: created PetStore:user
3 message(s)
id : integer ?
category : object ?
id : integer ?
name : string ?
name : string
photoUrls[] : string
tags[] : object ?
id : integer ?
name : string ?
status : string ?
生成了三个 DocType —— PetStore:pet、PetStore:order 和 PetStore:user —— 它们的名称取自根节点属性。 $ref 在整个文件范围内进行解析,因此 pet 的 category 和 tags() 被展开为 Category 和 Tag 的字段; name 和 photoUrls 位于 required 中,因此为必填项,其余所有字段均包含 ?。
为何此处使用 "properties":该文件的根节点精确列出了实际发送和接收的三种消息类型。 如果使用默认的 "defs"(FromBundleFile("PetStore", path)),将会生成六个以模型键命名的文档类型(PetStore:Pet、----215-----、PetStore:Tag……),其中包括仅被引用的片段(如 Category 和 Tag),这些片段绝不会单独发送。 相比之下,FromFile("PetStore", "All", path) 将整个文件视为一种消息类型,并提供一个包含三个可选对象字段的 DocType——这仅在消息确实以该信封形式到达时才有用。
情况 4:DTL 映射
使用案例 3 中的 PetStore:pet DocType,在 DTL 编辑器中打开 Demo.JVDoc.DT:源类和目标类均为 JVDoc.Document,两个 DocType 均为 PetStore:pet,而 create 则是 copy。 该映射将 :OtherPet 追加到 name 之后,并针对 tags() 添加一个 foreach 循环,使每个标签名前缀为 O:。

编辑器保存的类如下所示:
Class Demo.JVDoc.DT Extends Ens.DataTransformDTL [ DependsOn = JVDoc.Document ]
{
Parameter GENERATEEMPTYSEGMENTS = 0;
Parameter IGNOREMISSINGSOURCE = 1;
Parameter REPORTERRORS = 1;
Parameter TREATEMPTYREPEATINGFIELDASNULL = 0;
XData DTL [ XMLNamespace = "http://www.intersystems.com/dtl" ]
{
<transform sourceClass='JVDoc.Document' targetClass='JVDoc.Document' sourceDocType='PetStore:pet' targetDocType='PetStore:pet' create='copy' language='objectscript' >
<assign value='source.{name}_" :OtherPet" ' property='target.{name}' action='set' />
<foreach property='source.{tags()}' key='k1' >
<assign value='"O:"_source.{tags(k1).name}' property='target.{tags(k1).name}' action='set' />
</foreach>
</transform>
}
}
在终端中运行该转换:
Set src = ##class(JVDoc.Document).Create("{""id"":10,""name"":""Doggie"",""category"":{""id"":1,""name"":""Dogs""},""photoUrls"":[""https://example.com/doggie.png""],""tags"":[{""id"":1,""name"":""Friendly""},{""id"":2,""name"":""Small""}],""status"":""available""}", "PetStore:pet", .sc)
Set sc = ##class(Demo.JVDoc.DT).Transform(src, .tgt)
Write $System.Status.IsOK(sc), !
Write tgt.DocType, !
Write tgt.GetObj().%ToJSON(), !
Write src.GetObj().%ToJSON(), !
1
PetStore:pet
{"id":10,"name":"Doggie :OtherPet","category":{"id":1,"name":"Dogs"},"photoUrls":["https://example.com/doggie.png"],"tags":[{"id":1,"name":"O:Friendly"},{"id":2,"name":"O:Small"}],"status":"available"}
{"id":10,"name":"Doggie","category":{"id":1,"name":"Dogs"},"photoUrls":["https://example.com/doggie.png"],"tags":[{"id":1,"name":"Friendly"},{"id":2,"name":"Small"}],"status":"available"}
几点说明:
create='copy':目标文件最初是源文件的副本,因此未映射的字段(id、category、----208-----、status以及每个标签的id)将原样保留,数字仍保持为数字;目标文档类型取自targetDocType。 使用create='new'时,目标仅包含已映射的字段。foreach property='source.{tags()}' key='k1':k1依次取值 1、2、……,而循环体使用tags(k1).name来引用当前元素。 此处需要使用 foreach,因为每个值都必须添加前缀;如果你只想原样复制数组,则双侧()赋值source.{tags().name}→target.{tags().name}会自动逐个元素展开,无需使用 foreach。- 源消息不会被修改;目标是一个独立的新文档。
情况 5:路径读取
Set d = ##class(JVDoc.Document).%New("{""basics"":{""name"":""A"",""profiles"":[{""network"":""GitHub""}]},""work"":[{""name"":""X"",""highlights"":[""h1"",""h2""]}]}")
Write d.GetValueAt("basics.name"), !
Write d.GetValueAt("basics.profiles(1).network"), !
Write d.GetValueAt("work()"), !
Write d.GetValueAt("work(1).highlights(2)"), !
A
GitHub
1
h2
情况 6:搜索表
使用案例 3 中的 PetStore:pet DocType,按类别、名称和标签对宠物进行索引。定义搜索表类(其表示法与该产品自身的 HL7 搜索表相同,路径用方括号括起):
Class Demo.JVDoc.SearchTablePet Extends (%Persistent, Ens.VDoc.SearchTable)
{
Parameter DOCCLASS = "JVDoc.Document";
XData SearchSpec [ XMLNamespace = "http://www.intersystems.com/EnsSearchTable" ]
{
<Items>
<Item DocType="" PropName="Category">[category.name]</Item>
<Item DocType="" PropName="Name">[name]</Item>
<Item DocType="" PropName="Tag">[tags().name]</Item>
</Items>
}
}
DOCCLASS 指定了要建立索引的文档类;每个 Item 定义了一个可搜索属性,----247----- 是其在“消息查看器”中的显示名称,方括号内的路径是 JVDoc 路径——tags().name 遍历每个标签并获取其名称。 DocType="" 表示该项适用于所有路由到此搜索表的文档。
搜索表本身并不决定要索引哪些文档——这由绑定决定。当 JVDoc.Document 被保存时,它会在绑定表中查找其 DocType;如果已绑定搜索表,则调用该搜索表,否则不会写入索引行。 使用 SetSearchTable 设置绑定:
Write $System.Status.IsOK(##class(JVDoc.Schema).SetSearchTable("PetStore:pet", "Demo.JVDoc.SearchTablePet")), !
绑定键也可以写成 PetStore:*(该类别中的每个文档类型)或 *(所有文档类型)。 如果一个搜索表绑定到多个 DocType,而您希望某个特定项仅适用于其中之一,请将该项的 DocType 设置为完整的 PetStore:pet,或仅设置为名称部分 pet(名称为 pet 的任何类别);请注意,以冒号结尾且没有名称的 PetStore: 不会被视为通配符。
保存一个宠物文档,然后查询该表(Demo.JVDoc 包的 SQL 模式为 Demo_JVDoc):
Set d = ##class(JVDoc.Document).Create("{""id"":10,""name"":""Doggie"",""category"":{""id"":1,""name"":""Dogs""},""photoUrls"":[""https://example.com/doggie.png""],""tags"":[{""id"":1,""name"":""Friendly""},{""id"":2,""name"":""Small""}],""status"":""available""}", "PetStore:pet", .sc)
Do d.%Save()
Set rs = ##class(%SQL.Statement).%ExecDirect(, "SELECT PropId, PropValue FROM Demo_JVDoc.SearchTablePet WHERE DocId = ? ORDER BY PropId, PropValue", d.%Id())
While rs.%Next() { Write rs.%Get("PropId"), " ", rs.%Get("PropValue"), ! }
1 dogs
2 doggie
3 friendly
3 small
Category 是属性 1,Name 是属性 2,Tag 是属性 3;两个标签各自占据一行,且值均转换为小写。 要在“消息查看器”中进行筛选,消息必须已通过生产环境(搜索仅适用于带有消息头的信息):在“扩展条件”中添加一个“SearchTable Field”,选择类 Demo.JVDoc.SearchTablePet 以及条件 Tag = "Small" —— "small" 和 "Small" 均可找到相同的消息。
案例 7:路由规则与完整的端到端测试
使用上述案例中的 PetStore:pet 和 Demo.JVDoc.DT:狗的消息将原样转发,其他宠物消息需先经过 DTL,而验证失败的消息则在入口点被拦截。
-
生产环境中已包含两个业务操作:
Demo.JVDoc.BO和Demo.JVDoc.BOBadMsg。这两个操作仅会写入一条日志条目。 -
在规则编辑器中打开规则
Demo.JVDoc.RoutingRule。其规则类型为Virtual Document Message Routing Rule。该规则已有一个条目:该约束将“消息类”设置为
JVDoc.Document,“架构类别”设置为PetStore,并将“文档名称”设置为pet,从而将其限制为 DocTypePetStore:pet消息——docCategory和docName是冒号两侧的 DocType 的两部分。双击现有条件;在表达式编辑器中输入
Document.{,字段树随即显示。将
Document.{category.name}="Dogs"设置为发送至Demo.JVDoc.BO;在otherwise分支中,在发送至Demo.JVDoc.BO之前应用转换Demo.JVDoc.DT,然后“发送至”Demo.JVDoc.BO。编译该规则。
规则编辑器保存的类如下所示(
production是您自己的生产规则名称):Class Demo.JVDoc.RoutingRule Extends Ens.Rule.Definition { Parameter RuleAssistClass = "EnsLib.MsgRouter.VDocRuleAssist"; XData RuleDefinition [ XMLNamespace = "http://www.intersystems.com/rule" ] { <ruleDefinition alias="" context="EnsLib.MsgRouter.VDocRoutingEngine" production="Demo.JVDoc.Production"> <ruleSet name="" effectiveBegin="" effectiveEnd=""> <rule name="" disabled="false"> <constraint name="msgClass" value="JVDoc.Document"></constraint> <constraint name="docCategory" value="PetStore"></constraint> <constraint name="docName" value="pet"></constraint> <when condition="Document.{category.name}="Dogs"" comment=""> <send transform="" target="Demo.JVDoc.BO"></send> </when> <otherwise> <send transform="Demo.JVDoc.DT" target="Demo.JVDoc.BO"></send> </otherwise> </rule> </ruleSet> </ruleDefinition> } } -
该生产规则已包含一个业务流程
Demo.JVDoc.Router,其类为JVDoc.ValidatingRouter,且业务规则名称设置为Demo.JVDoc.RoutingRule。 -
在
Demo.JVDoc.Router上设置:Validation=dmall启用开关
Act On Validation Error。Bad Message Handler=Demo.JVDoc.BOBadMsg。
-
运行测试。
**选项 A — Postman:**生产环境中已有一个名为
PetStore、类为JVDoc.REST.GenericService的业务服务。 该容器在http://127.0.0.1:55773/PetStoreService/PetStore处暴露了一个无需身份验证的 REST 端点。将 JSON 消息放入请求正文中,并向该地址发送 POST 请求。**选项 B — 生产环境测试:**点击
Production Settings,启用Testing Enabled;选择业务流程Demo.JVDoc.BP,点击“操作”选项卡下的Test按钮,并将 JSON 粘贴到“StringValue”字段中。无论采用哪种方式,请发送以下四条消息(DocType
PetStore:pet):
| **消息** | **预期** |
|---|---|
| `{"id":11,"name":"Rex","category":{"name":"Dogs"},"photoUrls":[],"tags":[{"id":1,"name":"Labrador"},{"id":2,"name":"Male"}]}` | 符合规则;以 `Demo.JVDoc.BO` 形式原样到达 |
| `{"id":12,"name":"Tom","category":{"name":"Cats"},"photoUrls":[]}` | 取 `otherwise`;经过 `Demo.JVDoc.DT` 后到达 `Demo.JVDoc.BO`,`name` 变为 `Tom :OtherPet` |
| `{"id":13,"category":{"name":"Dogs"},"photoUrls":[]}` | 验证失败;将 `Demo.JVDoc.BOBadMsg` 传递给 `name: required field is missing` 并附带错误 |
| `{"id":14,"name":"Kitty","category":{"name":"Cats"},"photoUrls":[],"tags":[{"id":1,"name":"Persian"},{"id":2,"name":"Male"}],"status":"lost"}` | 验证失败;将错误 `status: value 'lost' is not in the allowed list` 传递给 `Demo.JVDoc.BOBadMsg` |
屏幕截图:测试消息 1(Rex / Dogs)的可视化跟踪
屏幕截图:测试消息 2 的可视化追踪(Tom / Cats → DT → 名称“Tom :OtherPet”)
关闭开关 Act On Validation Error 后,错误消息将被静默丢弃,路由过程本身不会报告错误。
-
搜索消息。
同时使用“VDoc 属性路径”和“搜索表”来查找上述测试中的消息。在“消息查看器”中,首先将搜索条件“类型”从“会话开始”更改为“全部”,以便包含所有消息。
然后添加“扩展条件”:
对于VDoc 属性路径搜索:将条件类型设置为“VDoc 属性路径”,类设置为
JVDoc.Document,然后在条件中选择文档类型和属性路径。请记得点击“搜索”。
对于**“搜索表**”搜索:将条件类型设置为“搜索表字段”,类设置为
Demo.JVDoc.SearchTablePet,然后在条件中选择字段和值。
实施说明
这些行为是从产品源代码和运行时日志中读取的;如果您想自己构建一个类似的虚拟文档,这些信息可能会有所帮助:
- DTL 编辑器会调用
GetContentArray至Ens.VDoc.Utils.GetInfoArray。它传递的pContents是一个全局引用名称,必须将字段树写入该引用中,而非将其清空。 - 规则编辑器中的字段补全功能要求文档类上的
SCHEMACLASS参数不为空,并且会调用该类的ResolveSchemaTypeToDocType方法。 - DTL 生成的
CopyValues调用有 8 个参数;()的逐元素展开功能在此方法中实现。 - 为搜索表生成的
IndexDoc会调用文档的FindSegmentValuesArray;返回数组的下标即为索引值。 - 消息查看器中的 VDoc 属性路径条件会打开每个候选消息并调用
GetValuesArray(path, , .arr, .sc, .long),此时路径中的 DocType 和大括号已被去除。 这些值是arr的下标;长度超过下标限制(230 个字符或更多)的值会在该处被截断,同时在long(n)中完整返回。 - 入口点的验证是
VDocRoutingEngine的一个子类,该子类重写了OnValidate;失败后的处理由两个设置Bad Message Handler和Act On Validation Error决定。
这些都不是已记录的接口,因此升级 IRIS 后,建议重新运行测试用例并逐一检查每个编辑器。
已知限制
- 根节点必须是一个 JSON 对象。
- 属性名称中包含
.、(、)、{或}的字段无法通过路径访问(但这些字段仍会作为消息内容传递)。 - DTL 树中不会展开嵌套数组;整个值将作为 JSON 文本进行复制(验证会检查其元素)。
- 不支持 JSON Schema 中的外部引用、patternProperties 和 format。
- 超过 18 位的数字会丢失精度。
- 字段树每个节点存储一种类型。当一个节点可能属于不同类型(对象、数组或标量),或者 JSON Schema 省略了类型,或者在类型数组或 oneOf/anyOf 中列出了不兼容的类型时,其类型为
any:当源和目标均为JVDoc.Document时,DTL 会按其真实的 JSON 类型复制该值,且验证不会检查any字段的类型。 在编辑器中,any节点是一个没有子字段的普通节点,因此要映射其内容,必须将其声明为对象并列出其属性;搜索表仅对any字段的标量值进行索引。 当仅标量类型不同(字符串与数字)时,JSON Schema 类型数组会取第一个非空类型,而采样推断则根据多数类型进行判定。
代码与反馈
来源:https://github.com/ISCQP/JSONVDoc.git
欢迎在评论中提供反馈:您的 JSON 接口中有哪些结构目前尚无法处理?

