When you're building .NET APIs you're comfortably surrounded by tooling for generating data and operation contract documentation automatically for you. The combination of SwaggerGen and SwaggerUI can automatically present consumers of your API with everything they need to understand the interface and to scaffold clients/DTOs - all based on the generated OpenAPI specification file, without much work as the developer. However when you're building something that isn't an "API" in the sense of "HTTP Request/Response", such as building serverless functions that process incoming messages you lose an awful lot of that tooling. In some eventing systems (such as Kafka) you have a schema registry, so you can use that to enforce validity and versioning of message data contracts for producers and consumers of that data (for example with an Avro schema). For simpler setups with no schema registry, it's still nice to have automatically generated documentation and schemas based on your source code. The reason I suggest generating your documentation for source is it ensures correctness - there's only one thing worse than missing documentation and that's wrong documentation. Also assuming you're versioning your software you'll have corresponding versioned documentation that sits alongside. The two goals of my approach are:
  • Produce JSON schema files for machine consumption
  • Produce Markdown files for human consumption

JSON Schema

Using JSON schemas are a great way to share data contracts in a language agnostic way, so consumers in any language can scaffold types from your schemas. They can also be used to validate incoming JSON before de-serializing it. Not only that but they plug into the OpenAPI specification, so you can reference them in your spec file (for example if you're exposing the data contract via an API Gateway endpoint). You can easily generate a JSON schema for a C# type using NJsonSchema, for example:

var typesToGenerate = GetTypesToGenerate(); // todo: implement creating the list of types, e.g. using typeof(MyDotnetModels.SomeType).Assembly.GetTypes()

if (!Directory.Exists("./generated"))
{
    Directory.CreateDirectory("./generated");
}

foreach (var type in typesToGenerate)
{
    var schema = JsonSchema.FromType(type);

    // add required to all non-nullable props
    foreach (var propertyInfo in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
    {
        var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
        if (!isNullable)
        {
            schema.RequiredProperties.Add(propertyInfo.Name);
        }
    }
    
    var schemaJson = schema.ToJson();
    
    File.WriteAllText($"./generated/{type.Name}.schema.json", schemaJson);
}
Build that code snippet into a console app (e.g. called JsonSchemaGen) and you can now execute it whenever your build your source code for deployment and it will generate JSON schema files in the bin/generated folder.

Markdown

Now that we have JSON schemas, it's easy to generate markdown files using @adobe/jsonschema2md. Simply pass the location of schema files with any configurations of your choice, e.g.

npx -y @adobe/jsonschema2md -h false \
	-d [path_to_schema_files] \
	-o markdown -x -
That will generated a README.md and md files for each schema, with human readable content describing your data contracts.

Using a makfile to combine it all

This part is optional, but it's nice to have the commands necessary to perform the above steps checked in to source control and to use the power of "make" to run the necessary steps only when things change, e.g.

.PHONY: clean build

schemas: [relative_path_to_dotnet_source]/MyDotnetModels
	cd JsonSchemaGen && \
	dotnet build -c Release && \
	cd bin/Release/net8.0 && ./JsonSchemaGen && \
	mkdir -p ../../../../schemas && mv generated/* ../../../../schemas/
	
markdown: schemas
	npx -y @adobe/jsonschema2md -h false \
	-d ./schemas \
	-o markdown -x -
	
build: markdown
	
clean:
	rm -rf schemas markdown
Summary of the steps:
  • schemas - depends on the dotnet models source code directory, so this runs whenever any file in that directory changes
    • Build the console app (including a reference to models) in release mode
    • Execute the built console app
    • Moves the generated files in another folder to make them easier to find
  • markdown - depends on the schemas, so this runs whenever the schemas change
    • Use npx to execute the @adobe/jsonschema2md and output to a directory called 'markdown'
You can now incorporate "make build" of your documentation makefile into your CI process and store the "markdown" and "schemas" directories as build artifacts, alongside your system build artifacts. Now they are ready to be shipped with the system when it's released (e.g. put in a storage account, or hosted as a static website - for example).