Events from Rust, Go, Java and .NET
document:docs/events/other-languagesWrite the schemas from a test with each language's own generator, commit them, and scan them like any JSON Schema.
autocatalog reads contract files, so a service in any language joins the
catalog once its schemas are on disk. Rust, Go, Java and .NET each have a
generator that writes JSON Schema from a type and OpenAPI from routes. Call
them from a test that writes contracts/, and commit what it writes:
contracts/ openapi.json the HTTP API, for the site's API reference events/payment.completed.json one JSON Schema per eventx-autocatalog names the event: id, version, domain, producers and consumers,
as in JSON Schema. Where the generator can’t add it
easily, put the same block in a sidecar file beside the schema,
payment.completed.autocatalog.json.
CI runs the test and fails if it changed anything, so the committed files always match the code:
- run: cargo test --test contracts # or go test, ./gradlew test, dotnet test- run: git diff --exit-code contracts/- run: npx autocatalog drift --base-ref origin/main --root .examples/frameworks/
has the same payments service in all four languages. CI checks that each one
produces the same catalog.
schemars for events, and openapi-axum or utoipa for the API.
#[derive(Serialize, JsonSchema)]#[schemars(extend("x-autocatalog" = { "id": "payment.completed", "version": "1.0.0", "domain": "payments", "producers": ["PaymentService"], "consumers": ["LedgerService"]}))]#[serde(rename_all = "camelCase")]pub struct PaymentCompleted { pub payment_id: String, pub amount: f64, pub currency: String,}
// tests/contracts.rs#[test]fn contracts() { let (_, openapi) = payments::app().split(); write("contracts/openapi.json", &openapi); write("contracts/events/payment.completed.json", &schemars::schema_for!(PaymentCompleted));}invopop/jsonschema for events, and
huma for the API. Fields without omitempty are
required.
func (PaymentCompleted) JSONSchemaExtend(s *jsonschema.Schema) { s.Extras = map[string]any{"x-autocatalog": map[string]any{ "id": "payment.completed", "version": "1.0.0", "domain": "payments", "producers": []string{"PaymentService"}, "consumers": []string{"LedgerService"}, }}}
func TestContracts(t *testing.T) { write(t, "contracts/openapi.json", New(http.NewServeMux()).OpenAPI()) r := jsonschema.Reflector{DoNotReference: true} write(t, "contracts/events/payment.completed.json", r.Reflect(&PaymentCompleted{}))}victools/jsonschema-generator
for events, and springdoc for a Spring Boot API. The
test starts the app and saves /v3/api-docs. Pin servers with
@OpenAPIDefinition, or the random test port ends up in the file. Declare
contracts/ as an output of the test task, tasks.test { outputs.dir("contracts") },
or Gradle skips the test as up to date and a stale file passes. The event
metadata lives in a sidecar.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)class ContractsTest { @Value("${local.server.port}") int port;
@Test void contracts() throws Exception { write("contracts/openapi.json", get("http://localhost:" + port + "/v3/api-docs"));
var config = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2020_12, OptionPreset.PLAIN_JSON); config.forFields().withRequiredCheck(field -> true); // every record component var schema = new SchemaGenerator(config.build()).generateSchema(PaymentCompleted.class); write("contracts/events/payment.completed.json", schema.toPrettyString()); }}{ "id": "payment.completed", "version": "1.0.0", "domain": "payments", "producers": ["PaymentService"], "consumers": ["LedgerService"]}JsonSchemaExporter from System.Text.Json for events, and
Microsoft.AspNetCore.OpenApi for a minimal API. Neither needs a third-party
package. Turn on RespectNullableAnnotations and strict number handling, or
the web defaults widen every number to ["string", "number"].
static readonly JsonSerializerOptions Web = new(JsonSerializerDefaults.Web){ RespectNullableAnnotations = true, NumberHandling = JsonNumberHandling.Strict, TypeInfoResolver = new DefaultJsonTypeInfoResolver(),};
[Fact]public async Task Contracts(){ using var factory = new WebApplicationFactory<Program>(); Write("contracts/openapi.json", JsonNode.Parse(await factory.CreateClient().GetStringAsync("/openapi/v1.json"))!);
var schema = Web.GetJsonSchemaAsNode(typeof(PaymentCompleted), new() { TreatNullObliviousAsNonNullable = true }); schema["$schema"] = "https://json-schema.org/draft/2020-12/schema"; schema["x-autocatalog"] = new JsonObject { ["id"] = "payment.completed", ["version"] = "1.0.0", /* … */ }; Write("contracts/events/payment.completed.json", schema);}Build output
Section titled “Build output”The scanner skips target/, bin/, obj/ and build/. A schema copied into
build output isn’t read twice.