• 你知道.NET 9中JsonSchemaExporter的用途吗?
  • 发布于 1个月前
  • 55 热度
    0 评论
  • 乌龙山
  • 0 粉丝 41 篇博客
  •   
前言
.NET 9 Preview 6 中引入了一个 JsonSchemaExporter,我们可以借助它根据类型来生成 json schema,之前我们有写过一篇文章使用 JsonSchema 来验证 API 的 response 使用 JsonSchema 验证 API 的返回格式,有了这个 API 之后就可以更方便地生成 JsonSchema 了

例子
首先我们准备一下类型用以测试:
public class Job
{
   // 堆代码 duidaima.com
    public int Id { get; set; }
    public string Title { get; set; } = string.Empty;
    public string? Description { get; set; }
}
从 JsonSerializeOptions 获取类型的 json schema 结构
var type = typeof(Job);
var defaultSchemaNode = JsonSchemaExporter.GetJsonSchemaAsNode(
    JsonSerializerOptions.Default, type
    );
Console.WriteLine(JsonSerializer.Serialize(defaultSchemaNode, JsonSerializerOptions.Web));
我们可以使用 JsonSchemaExporter.GetJsonSchemaAsNode 来获取 jsonSchema,输出结果如下:
{"type":["object","null"],"properties":{"Id":{"type":"integer"},"Title":{"type":"string"},"Description":{"type":["string","null"]}}}
这个方法定义为扩展方法,我们也可以通过扩展方法的方式来使用,JsonSchema 导出之后是一个 JsonNode 对象,大小写命名规则等由 JsonSerializerOptions 来决定,所以需要一个 JsonSerializerOptions 参数,我们再来看下使用不同的 JsonSerializerOptions 的结果有何不同
var schemaNode = JsonSerializerOptions.Web.GetJsonSchemaAsNode(typeof(Job));
Console.WriteLine(JsonSerializer.Serialize(schemaNode, JsonSerializerOptions.Web));
和前面相比,这次我们使用了 JsonSerializerOptions.Web, 会使用 CamelCase 的命名规则, 输出结果如下:
{"type":["object","null"],"properties":{"id":{"type":["string","integer"],"pattern":"^-?(?:0|[1-9]\\d*)$"},"title":{"type":"string"},"description":{"type":["string","null"]}}}
可以看到此时,我们的属性名成变成了小写,另外由于 Web option 默认允许字符串转成数值,所以能看到我们的 id,允许的 type 除了 integer 之外还有 string,不过 string 也多了一个数字的正则表达式规则校验,这也说明了 JsonSerializerOptions 对 jsonSchema 的影响是比较大的。如果我想要 title 必填的话要怎么做呢,可以把 Title 设置为 required, 添加一个 required 修饰符即可
public class Job
{
    public int Id { get; set; }
    public required string Title { get; set; }
    public string? Description { get; set; }
}
此时输出结果就变成了下面这样:
{"type":["object","null"],"properties":{"id":{"type":["string","integer"],"pattern":"^-?(?:0|[1-9]\\d*)$"},"title":{"type":"string"},"description":{"type":["string","null"]}},"required":["title"]}
可以看到在最后增加了一个 required 属性,里面有一个 title 表示 title 属性必填,没有的话 json schema 验证应该失败

除此之外,我们还可以在导出的时候做一些自定义的操作,示例如下:

var exporterOptions = new JsonSchemaExporterOptions
{
    TransformSchemaNode = (context, jsonNode) =>
    {
        var node = jsonNode.DeepClone();
        var idNames = new[] { "id", "Id" };

        if (node["properties"] is not JsonObject propertiesNode)
            return node;
        // 堆代码 duidaima.com
        foreach (var idName in idNames)
        {
            if (propertiesNode[idName] is JsonObject)
            {
                var requiredNode = node["required"];
                if (requiredNode is JsonArray jsonArrayNode)
                {
                    var requiredProperties = JsonSerializer.Serialize(jsonArrayNode.Select(x => x.GetValue<string>()).Append(idName));
                    jsonArrayNode.ReplaceWith(JsonSerializer.Deserialize<JsonArray>(requiredProperties));
                }
                else
                {
                    node["required"] = JsonSerializer.Deserialize<JsonArray>($"""["{idName}"]""");
                }
            }

        }
        return node;
    }
};
var schemaNode3 = JsonSerializerOptions.Web.GetJsonSchemaAsNode(typeof(Job), exporterOptions);
Console.WriteLine(JsonSerializer.Serialize(schemaNode3, JsonSerializerOptions.Web));
这里我们在生成的 jsonSchema node 的基础之上,如果属性名称是 id 或者 Id 的话就将它添加到 required 中或者创建一个 required 并将 id 属性名添加进去,输出结果如下:
{"type":["object","null"],"properties":{"id":{"type":["string","integer"],"pattern":"^-?(?:0|[1-9]\\d*)$"},"title":{"type":"string"},"description":{"type":["string","null"]}},"required":["title","id"]}
这里可以看到针对前面的输出,required 里多个 id 属性。我们再来测试一下 Id 以及没有 required 属性的情况,我们将 required 修饰符给去掉,再加入 exporterOptions 和第一次的输出结果做个对比
var schemaNode4 = JsonSerializerOptions.Default.GetJsonSchemaAsNode(typeof(Job), exporterOptions);
Console.WriteLine(JsonSerializer.Serialize(schemaNode4, JsonSerializerOptions.Web));
此时输出结果如下:
{"type":["object","null"],"properties":{"Id":{"type":"integer"},"Title":{"type":"string"},"Description":{"type":["string","null"]}},"required":["Id"]}
可以看到输出结果里有了 required, 再来用 json schema 验证下看看。

这个示例只是为了说明可以自定义,实际使用可以直接添加一个 required 修饰符即可。


其它
目前的 JsonSchema 支持还比较早期,对于复杂的需求可能还需要自己扩展,比如说设置 schema 需要类似前面示例一样自己扩展下,在 .NET 10 里应该还会继续优化和增强。

参考
https://github.com/WeihanLi/SamplesInPractice/blob/main/net9sample/Net9Samples/JsonSample.cs
https://github.com/dotnet/runtime/issues/102788
https://github.com/dotnet/runtime/pull/103322
https://github.com/dotnet/runtime/issues/105769
https://www.jsonschemavalidator.net/
用户评论