Repository navigation
Feature request: Model keySchemaMetadata / valueSchemaMetadata in the Parser Kafka schemas #5868
Description
Use case
When a Kafka event source mapping uses a schema registry, Lambda adds keySchemaMetadata and valueSchemaMetadata (dataFormat, schemaId) to each record (Lambda docs: Kafka event payload). I parse MSK / self-managed Kafka events with KafkaMskEventSchema / KafkaSelfManagedEventSchema and then need the schema ID and format for downstream deserialization and record-keeping. After parsing, both fields are gone from the result and from the inferred type.
Location (aws-powertools/powertools-lambda-typescript @ default branch faa8220):
packages/parser/src/schemas/kafka.ts:7-32—KafkaRecordSchema, shared by the MSK and self-managed event schemas.
Current code:
const KafkaRecordSchema = z.object({
topic: z.string(),
partition: z.number(),
offset: z.number(),
timestamp: z.number(),
timestampType: z.string(),
key: z
.string()
.transform((key) => {
return Buffer.from(key, 'base64').toString();
})
.optional(),
value: z.string().transform((value) => {
return Buffer.from(value, 'base64').toString();
}),
headers: z.array(
z.record(
z.string(),
// Lambda sends header bytes as signed Java bytes, so bytes >= 128 arrive negative.
// Buffer.from masks each number to a byte, and decoding matches @aws-lambda-powertools/kafka.
z.array(z.number().int().min(-128).max(255)).transform((value) => {
return Buffer.from(value).toString('utf8');
})
)
),
});Problem: KafkaRecordSchema does not declare keySchemaMetadata / valueSchemaMetadata, and the z.object default strips undeclared keys, so both fields are dropped from the parsed record and are absent from the inferred type.
Reproduction:
import { KafkaMskEventSchema } from '@aws-lambda-powertools/parser/schemas/kafka';
const event = {
eventSource: 'aws:kafka',
eventSourceArn: 'arn:aws:kafka:us-east-1:123456789012:cluster/demo/abcd',
bootstrapServers: 'b-1.demo:9092',
records: {
'mytopic-0': [{
topic: 'mytopic', partition: 0, offset: 15,
timestamp: 1545084650987, timestampType: 'CREATE_TIME',
key: 'cmVjb3JkS2V5', value: 'eyJrZXkiOiJ2YWx1ZSJ9', headers: [],
valueSchemaMetadata: { dataFormat: 'AVRO', schemaId: '1234' },
keySchemaMetadata: { dataFormat: 'AVRO', schemaId: '5678' },
}],
},
};
const record = KafkaMskEventSchema.parse(event).records['mytopic-0'][0];
console.log(record.valueSchemaMetadata); // undefined (and not part of the inferred type)Vitest output from the equivalent unit test
FAIL tests/unit/schema/kafkaSchemaMetadata.test.ts > Schema: Kafka schema metadata > preserves valueSchemaMetadata when parsing a Kafka MSK event
AssertionError: expected { topic: 'mytopic', …(7) } to have property "valueSchemaMetadata"
FAIL tests/unit/schema/kafkaSchemaMetadata.test.ts > Schema: Kafka schema metadata > preserves keySchemaMetadata when parsing a Kafka MSK event
AssertionError: expected { topic: 'mytopic', …(7) } to have property "keySchemaMetadata"
FAIL tests/unit/schema/kafkaSchemaMetadata.test.ts > Schema: Kafka schema metadata > preserves schema metadata when parsing a self-managed Kafka event
AssertionError: expected { topic: 'mytopic', …(7) } to have property "valueSchemaMetadata"
A fourth case (event without metadata) passed. Node.js v24.22.1.
Environment: tested on main at 219d973 with Node.js v24.22.1; the schema file is byte-identical on current main faa8220.
Solution/User Experience
Already implemented in aws-powertools/powertools-lambda-python
PR: aws-powertools/powertools-lambda-python#6821 — "feat(kafka): New Kafka utility" (merged 2025-06-18T18:10:41Z, merge commit e920f08519d).
This PR introduced Python's Kafka utility; the part relevant here is the Parser model change, which added optional keySchemaMetadata / valueSchemaMetadata to KafkaRecordModel (still present today: models/kafka.py#L64-L73). The rest of the PR (deserializers, consumer records, data-class changes) is unrelated to Parser.
How powertools-lambda-python implemented it:
--- a/aws_lambda_powertools/utilities/parser/models/kafka.py
+++ b/aws_lambda_powertools/utilities/parser/models/kafka.py
@@ -8,6 +8,11 @@
SERVERS_DELIMITER = ","
+class KafkaRecordSchemaMetadata(BaseModel):
+ dataFormat: str
+ schemaId: str
+
+
class KafkaRecordModel(BaseModel):
topic: str
partition: int
@@ -17,6 +22,8 @@ class KafkaRecordModel(BaseModel):
key: Optional[bytes] = None
value: Union[str, Type[BaseModel]]
headers: List[Dict[str, bytes]]
+ keySchemaMetadata: Optional[KafkaRecordSchemaMetadata] = None
+ valueSchemaMetadata: Optional[KafkaRecordSchemaMetadata] = None
# key is optional; only decode if not None
@field_validator("key", mode="before")Suggested fix
Add both fields as optional objects on KafkaRecordSchema, shared by the MSK and self-managed schemas, as powertools-lambda-python does for its Parser model. The shape mirrors the SchemaMetadata type in @aws-lambda-powertools/kafka (types.ts#L18-L30), and events without a schema registry parse as before:
diff --git a/packages/parser/src/schemas/kafka.ts b/packages/parser/src/schemas/kafka.ts
--- a/packages/parser/src/schemas/kafka.ts
+++ b/packages/parser/src/schemas/kafka.ts
@@ -1,6 +1,14 @@
import { z } from 'zod';
import type { KafkaMskEvent, KafkaSelfManagedEvent } from '../types/schema.js';
+/**
+ * Zod schema for the schema registry metadata of a Kafka record key or value.
+ */
+const KafkaSchemaMetadataSchema = z.object({
+ dataFormat: z.string(),
+ schemaId: z.string().optional(),
+});
+
/**
* Zod schema for a Kafka record from an Kafka event.
*/
@@ -29,6 +37,8 @@ const KafkaRecordSchema = z.object({
})
)
),
+ keySchemaMetadata: KafkaSchemaMetadataSchema.optional(),
+ valueSchemaMetadata: KafkaSchemaMetadataSchema.optional(),
});
const KafkaBaseEventSchema = z.object({Verified on 219d973 (Node.js v24.22.1, Vitest 5.0.5): the test above fails without this patch and passes with it; no other test results change.
I can submit a PR for this.
Alternative solutions
- Extend the exported
KafkaRecordSchemawith the two fields and rebuild the MSK / self-managed event schemas in user code: works, but every user re-implements the same documented fields and has to keep the event wrappers in sync. - Use
@aws-lambda-powertools/kafka's consumer instead of Parser: fine for deserialization, but not when the goal is Parser-based validation of the event.
Acknowledgment
- This feature request meets Powertools for AWS Lambda (TypeScript) Tenets
- Should this be considered in other Powertools for AWS Lambda languages? i.e. Python, Java, and .NET
Acknowledgment
- This feature request meets Powertools for AWS Lambda (TypeScript) Tenets
- Should this be considered in other Powertools for AWS Lambda languages? i.e. Python, Java, and .NET
Related issues checked
Checked: hits concern the Kafka consumer's tombstone handling (#4988, #5016), Kafka header decoding (#5830, #5839) and exporting record schema building blocks (#2811) — none add these fields to Parser.
Activity
powertools-for-aws-oss-automation commentedon Oct 30, 2026
Thanks for opening your first issue here! We'll come back to you as soon as we can.
In the meantime, check out our GitHub Discussions: Join the conversation
dreamorosi commentedon Oct 30, 2026
Hi @hidingmyname, thanks for this one too. Confirmed:
keySchemaMetadataandvalueSchemaMetadataare documented in the Lambda Kafka event payload when schema validation is configured on the event source mapping, andKafkaRecordSchemacurrently strips them. Powertools for AWS Lambda (Python) models both fields in its Parser, as you pointed out.Your proposed fix looks good to me, including keeping
schemaIdoptional. Python requires it, but our own@aws-lambda-powertools/kafkatreats it as optional (seeSchemaMetadatainpackages/kafka/src/types/types.tsand theschemaId === undefinedbranch in the Protobuf deserializer), and the Parser should not reject events that the Kafka utility accepts. Please mention this difference from Python in the PR description.I've assigned the issue to you. Please add these tests to the existing
describeblock inpackages/parser/tests/unit/schema/kafka.test.ts, using its existingrawEvent:All four currently fail on main, and the schema change from your proposal should make them pass.
KafkaMskEventandKafkaSelfManagedEventshould stay inferred from the schema, so no separate type change is needed, and please don't add or modify other tests unless coverage drops below 100%.