.NET 9 中的 JsonSchemaExporter

.NET 9 中的 JsonSchemaExporter

Intro

.NET 9 Preview 6 中引入了一个 JsonSchemaExporter,我们可以借助它根据类型来生成 json schema,之前我们有写过一篇文章使用 JsonSchema 来验证 API 的 response 使用 JsonSchema 验证 API 的返回格式,有了这个 API 之后就可以更方便地生成 JsonSchema 了

Samples

首先我们准备一下类型用以测试:

public class Job
{
    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 验证应该失败

a46ae9aee088497116d505c204459ab0.png

05c04b72f7eb657a1b80f6698b66e4bf.png

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

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;

        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 验证下看看

3d77d642447f81f4daedfb7ed2e0a148.png

9000ef94a00f037795d01a4483f72603.png

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

More

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

References

  • 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/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值