API Metadata
The discovery and trust plane — a searchable catalog of every API, topic, and table with ownership, automated lineage, and quality signals, so you can answer 'if I change this field, who breaks?' before you change it.
A contract that is enforced but undiscoverable is half useless. If a consumer can’t find your API and decide whether to trust it, they’ll build their own or screen-scrape yours — and the boundary you worked to establish quietly erodes. Metadata is the discovery and trust plane: a searchable catalog of every API, topic, and table, with ownership, lineage, and quality signals attached.
Discovery, lineage, and ownership
In this stack the catalog is OpenMetadata. It ingests from many sources — OpenAPI specs, the Apicurio registry, Kafka topics, Postgres schemas — through scheduled connectors, and builds one searchable catalog that stays current without manual upkeep. That last point matters: a wiki is stale the day after it’s written; an ingested catalog is not.
The real differentiator over a wiki is automated lineage. OpenMetadata knows that this topic feeds that table feeds this API — column- and topic-level. So it can answer the question that otherwise gets answered the hard way, in production:
if I change this field, who breaks?
— and answer it before you make the change, not after a downstream team’s dashboard goes blank. OpenMetadata derives this graph automatically from what it ingests — parsing schemas, topic configurations, and query history — so the lineage stays accurate as the system changes, with no one drawing a dependency diagram by hand.
What the catalog gives every consumer
Four concrete payoffs, each replacing a thing teams currently do by asking around:
- Discovery — search across APIs, topics, and tables, and find the owning team
and how to get access. No more hunting through Slack for who owns
inventory. - Trust — published SLOs, data-quality test results, freshness, and a criticality tier tell you whether this thing is safe to depend on.
- Lineage — column- and topic-level impact analysis answers the change-safety question proactively, for both producers and consumers.
- Ownership and governance — tags, PII classifications, and an accountable owner on every asset. This is also exactly what a compliance team needs, so the catalog does double duty.
Working with the catalog API
OpenMetadata exposes a REST API for everything the UI does. Two operations every service team reaches for: tagging an asset (mark a field as PII so governance knows about it) and querying lineage (find out who depends on a topic before changing its schema). Both are simple HTTP calls from any language:
// MetadataClient.java — tag an asset and query lineage via OpenMetadata REST API
// (JsonNode is tools.jackson.databind.JsonNode: Spring Boot 4 uses Jackson 3)
@Service
public class MetadataClient {
private static final String CATALOG = "http://openmetadata.svc/api/v1";
private final RestClient rest = RestClient.create();
public void tagAsPii(String tableId, String column) {
rest.patch()
.uri(CATALOG + "/tables/{id}", tableId)
.contentType(MediaType.valueOf("application/json-patch+json"))
.body(List.of(Map.of("op", "add",
"path", "/columns/" + column + "/tags/-",
"value", Map.of("tagFQN", "PII.Sensitive"))))
.retrieve().toBodilessEntity();
}
public JsonNode lineage(String topicFqn) {
return rest.get()
.uri(CATALOG + "/lineage/topic/name/{fqn}", topicFqn)
.retrieve().body(JsonNode.class); // upstream + downstream graph
}
}
// MetadataClient.java — Quarkus REST client for OpenMetadata
@RegisterRestClient(baseUri = "http://openmetadata.svc/api/v1")
public interface MetadataClient {
@PATCH @Path("/tables/{id}")
@Consumes("application/json-patch+json")
Response tagAsPii(@PathParam("id") String tableId, JsonArray patch);
@GET @Path("/lineage/topic/name/{fqn}")
JsonObject lineage(@PathParam("fqn") String topicFqn);
}
// MetadataClient.cs — tag an asset and query lineage via OpenMetadata REST API
public class MetadataClient(HttpClient http)
{
private const string Catalog = "http://openmetadata.svc/api/v1";
public async Task TagAsPiiAsync(string tableId, string column)
{
var patch = JsonSerializer.Serialize(new[] { new {
op = "add",
path = $"/columns/{column}/tags/-",
value = new { tagFQN = "PII.Sensitive" } } });
await http.PatchAsync($"{Catalog}/tables/{tableId}",
new StringContent(patch, Encoding.UTF8, "application/json-patch+json"));
}
public async Task<JsonDocument> LineageAsync(string topicFqn)
{
var json = await http.GetStringAsync(
$"{Catalog}/lineage/topic/name/{topicFqn}");
return JsonDocument.Parse(json); // upstream + downstream graph
}
}
import httpx
CATALOG = "http://openmetadata.svc/api/v1"
async def tag_as_pii(table_id: str, column: str):
async with httpx.AsyncClient() as client:
await client.patch(
f"{CATALOG}/tables/{table_id}",
headers={"Content-Type": "application/json-patch+json"},
json=[{"op": "add",
"path": f"/columns/{column}/tags/-",
"value": {"tagFQN": "PII.Sensitive"}}],
)
async def lineage(topic_fqn: str) -> dict:
async with httpx.AsyncClient() as client:
r = await client.get(f"{CATALOG}/lineage/topic/name/{topic_fqn}")
return r.json() # upstream + downstream graph
// metadata_client.h — tag an asset and query lineage via OpenMetadata REST API
#include <cpr/cpr.h>
inline void tag_as_pii(std::string_view table_id, std::string_view column) {
auto url = fmt::format("http://openmetadata.svc/api/v1/tables/{}", table_id);
auto patch = fmt::format(
R"([{{"op":"add","path":"/columns/{}/tags/-","value":{{"tagFQN":"PII.Sensitive"}}}}])",
column);
cpr::Patch(cpr::Url{url},
cpr::Header{{"Content-Type", "application/json-patch+json"}},
cpr::Body{patch});
}
inline std::string lineage(std::string_view topic_fqn) {
auto url = fmt::format(
"http://openmetadata.svc/api/v1/lineage/topic/name/{}", topic_fqn);
return cpr::Get(cpr::Url{url}).text; // upstream + downstream graph
}
// metadata_client.go — tag an asset and query lineage via OpenMetadata REST API
const catalog = "http://openmetadata.svc/api/v1"
func tagAsPII(ctx context.Context, tableID, column string) error {
patch, _ := json.Marshal([]map[string]any{{
"op": "add", "path": "/columns/" + column + "/tags/-",
"value": map[string]string{"tagFQN": "PII.Sensitive"},
}})
req, _ := http.NewRequestWithContext(ctx, "PATCH",
catalog+"/tables/"+tableID, bytes.NewReader(patch))
req.Header.Set("Content-Type", "application/json-patch+json")
_, err := http.DefaultClient.Do(req)
return err
}
func lineage(ctx context.Context, topicFQN string) (map[string]any, error) {
resp, err := http.Get(catalog + "/lineage/topic/name/" + topicFQN)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result) // upstream + downstream graph
return result, nil
}
Data contracts as code
The catalog is most valuable when its metadata lives in Git alongside the schemas it describes — data contracts as code. A data contract is a YAML or JSON document committed next to the schema that declares the asset’s owner, its SLO (freshness, completeness), its PII classification, and its criticality tier. The CI pipeline that gates schema changes also validates the contract, so ownership and quality expectations are version-controlled and reviewable, not hand-entered in a UI.
The throughline of the last two chapters: a contract is only as good as your ability to find it, trust it, and know who owns it. The registry makes the contract authoritative; the catalog makes it discoverable and accountable. Together they turn “we have contracts somewhere” into a platform people can actually build on.
Cross-check it yourself
The catalog’s value shows up as a question answered in seconds instead of a
day. With the stack running, search the catalog for the order.placed topic and
follow its lineage downstream — confirm you can see, without asking anyone, which
tables and APIs consume it and which team owns each. Then check that an asset
carries its owner and PII tags. Getting that impact list from the catalog rather
than from a post-incident retro is the whole point.
Verification status: conceptual chapter — no per-language runnable code. The discovery, lineage, and trust capabilities it describes are exercised against a running OpenMetadata catalog in the example stack.