.NET 9 Preview 6 中引入了一个 JsonSchemaExporter
,我们可以借助它根据类型来生成 json schema,之前我们有写过一篇文章使用 JsonSchema 来验证 API 的 response 使用 JsonSchema 验证 API 的返回格式,有了这个 API 之后就可以更方便地生成 JsonSchema 了
首先我们准备一下类型用以测试:
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 验证应该失败
除此之外,我们还可以在导出的时候做一些自定义的操作,示例如下:
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 验证下看看
这个示例只是为了说明可以自定义,实际使用可以直接添加一个 required
修饰符即可
目前的 JsonSchema 支持还比较早期,对于复杂的需求可能还需要自己扩展,比如说设置 schema 需要类似前面示例一样自己扩展下,在 .NET 10 里应该还会继续优化和增强