Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions schemaregistry/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@types/simple-oauth2": "^5.0.7",
"@types/validator": "^13.12.0",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"async-mutex": "^0.5.0",
"avsc": "^5.7.7",
"axios": "^1.13.5",
Expand Down
6 changes: 3 additions & 3 deletions schemaregistry/serde/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import Ajv, {ErrorObject} from "ajv";
import Ajv2019 from "ajv/dist/2019";
import Ajv2020 from "ajv/dist/2020";
import addFormats from "ajv-formats";
import * as draft6MetaSchema from 'ajv/dist/refs/json-schema-draft-06.json'
import * as draft7MetaSchema from 'ajv/dist/refs/json-schema-draft-07.json'
import {
Expand Down Expand Up @@ -272,13 +273,15 @@ async function toValidateFunction(
if (spec === 'http://json-schema.org/draft/2020-12/schema'
|| spec === 'https://json-schema.org/draft/2020-12/schema') {
const ajv2020 = new Ajv2020({ ...conf as JsonSerdeConfig, allErrors: true })
addFormats(ajv2020)
ajv2020.addKeyword("confluent:tags")
deps.forEach((schema, name) => {
ajv2020.addSchema(JSON.parse(schema), name)
})
fn = ajv2020.compile(json)
} else {
const ajv = new Ajv2019({ ...conf as JsonSerdeConfig, allErrors: true })
addFormats(ajv)
ajv.addKeyword("confluent:tags")
ajv.addMetaSchema(draft6MetaSchema)
ajv.addMetaSchema(draft7MetaSchema)
Expand Down Expand Up @@ -504,6 +507,3 @@ function disjoint(tags1: Set<string>, tags2: Set<string>): boolean {
}
return true
}



193 changes: 193 additions & 0 deletions schemaregistry/test/serde/json.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,63 @@ const messageSchema = `
}
}
`
const schemaWithFormats = `
{
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email"
Comment on lines +278 to +284
Copy link

Copilot AI Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

schemaWithFormats does not include a $schema declaration, so this test only exercises the non-2020-12 validation path in toValidateFunction. Since this PR adds addFormats in both Ajv2019 and Ajv2020 branches, add an additional test case (or extend this schema) that sets $schema to the 2020-12 URI to cover the Ajv2020 path as well.

Copilot uses AI. Check for mistakes.
},
"website": {
"type": "string",
"format": "uri"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"ipv4Address": {
"type": "string",
"format": "ipv4"
},
"uuid": {
"type": "string",
"format": "uuid"
}
},
"required": ["email", "createdAt"]
}
`
const schemaWithFormats2020_12 = `
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email"
},
"website": {
"type": "string",
"format": "uri"
},
"createdAt": {
"type": "string",
"format": "date-time"
},
"ipv4Address": {
"type": "string",
"format": "ipv4"
},
"uuid": {
"type": "string",
"format": "uuid"
}
},
"required": ["email", "createdAt"]
}
`

describe('JsonSerializer', () => {
afterEach(async () => {
Expand Down Expand Up @@ -516,6 +573,142 @@ describe('JsonSerializer', () => {

await expect(() => ser.serialize(topic, diffObj)).rejects.toThrow(SerializationError)
})
it('format validation', async () => {
let conf: ClientConfig = {
baseURLs: [baseURL],
cacheCapacity: 1000
}
let client = SchemaRegistryClient.newClient(conf)
let ser = new JsonSerializer(client, SerdeType.VALUE, {
useLatestVersion: true,
validate: true
})

let info: SchemaInfo = {
schemaType: 'JSON',
schema: schemaWithFormats
}

await client.register(subject, info, false)

let validObjectWithCorrectFormats = {
email: 'user@example.com',
website: 'https://example.com',
createdAt: '2024-01-15T10:30:00Z',
ipv4Address: '192.168.1.1',
uuid: '550e8400-e29b-41d4-a716-446655440000'
}
let bytes = await ser.serialize(topic, validObjectWithCorrectFormats)

let deser = new JsonDeserializer(client, SerdeType.VALUE, {})
let obj2 = await deser.deserialize(topic, bytes)
expect(obj2).toEqual(validObjectWithCorrectFormats)

let invalidEmailObj = {
email: 'not-an-email',
website: 'https://example.com',
createdAt: '2024-01-15T10:30:00Z'
}
await expect(() => ser.serialize(topic, invalidEmailObj)).rejects.toThrow(SerializationError)

let invalidUriObj = {
email: 'user@example.com',
website: 'not a uri',
createdAt: '2024-01-15T10:30:00Z'
}
await expect(() => ser.serialize(topic, invalidUriObj)).rejects.toThrow(SerializationError)

let invalidDateObj = {
email: 'user@example.com',
website: 'https://example.com',
createdAt: 'not-a-date'
}
await expect(() => ser.serialize(topic, invalidDateObj)).rejects.toThrow(SerializationError)

let invalidIpv4Obj = {
email: 'user@example.com',
website: 'https://example.com',
createdAt: '2024-01-15T10:30:00Z',
ipv4Address: '999.999.999.999'
}
await expect(() => ser.serialize(topic, invalidIpv4Obj)).rejects.toThrow(SerializationError)

let invalidUuidObj = {
email: 'user@example.com',
website: 'https://example.com',
createdAt: '2024-01-15T10:30:00Z',
uuid: 'not-a-uuid'
}
await expect(() => ser.serialize(topic, invalidUuidObj)).rejects.toThrow(SerializationError)
})
it('format validation 2020-12', async () => {
let conf: ClientConfig = {
baseURLs: [baseURL],
cacheCapacity: 1000
}
let client = SchemaRegistryClient.newClient(conf)
let ser = new JsonSerializer(client, SerdeType.VALUE, {
useLatestVersion: true,
validate: true
})

let info: SchemaInfo = {
schemaType: 'JSON',
schema: schemaWithFormats2020_12
}

await client.register(subject, info, false)

let validObjectWithCorrectFormats = {
email: 'user@example.com',
website: 'https://example.com',
createdAt: '2024-01-15T10:30:00Z',
ipv4Address: '192.168.1.1',
uuid: '550e8400-e29b-41d4-a716-446655440000'
}
let bytes = await ser.serialize(topic, validObjectWithCorrectFormats)

let deser = new JsonDeserializer(client, SerdeType.VALUE, {})
let obj2 = await deser.deserialize(topic, bytes)
expect(obj2).toEqual(validObjectWithCorrectFormats)

let invalidEmailObj = {
email: 'not-an-email',
website: 'https://example.com',
createdAt: '2024-01-15T10:30:00Z'
}
await expect(() => ser.serialize(topic, invalidEmailObj)).rejects.toThrow(SerializationError)

let invalidUriObj = {
email: 'user@example.com',
website: 'not a uri',
createdAt: '2024-01-15T10:30:00Z'
}
await expect(() => ser.serialize(topic, invalidUriObj)).rejects.toThrow(SerializationError)

let invalidDateObj = {
email: 'user@example.com',
website: 'https://example.com',
createdAt: 'not-a-date'
}
await expect(() => ser.serialize(topic, invalidDateObj)).rejects.toThrow(SerializationError)

let invalidIpv4Obj = {
email: 'user@example.com',
website: 'https://example.com',
createdAt: '2024-01-15T10:30:00Z',
ipv4Address: '999.999.999.999'
}
await expect(() => ser.serialize(topic, invalidIpv4Obj)).rejects.toThrow(SerializationError)

let invalidUuidObj = {
email: 'user@example.com',
website: 'https://example.com',
createdAt: '2024-01-15T10:30:00Z',
uuid: 'not-a-uuid'
}
await expect(() => ser.serialize(topic, invalidUuidObj)).rejects.toThrow(SerializationError)
})
it('cel field transform', async () => {
let conf: ClientConfig = {
baseURLs: [baseURL],
Expand Down