JSON Schema for Contract Engineers: Constraints, Composition, Dialects, and Validation Boundaries
Learn Java API Contract Engineering, Event Contract Engineering & Schema Governance - Part 019
JSON Schema contract engineering for Java systems: constraints, composition, dialects, $id, $ref, $defs, validation boundaries, OpenAPI alignment, and schema governance.
Part 019 — JSON Schema for Contract Engineers: Constraints, Composition, Dialects, and Validation Boundaries
Tujuan Pembelajaran
JSON Schema sering dipakai untuk validasi payload JSON, tetapi banyak engineer memperlakukannya sebagai daftar field dan required. Itu terlalu dangkal.
JSON Schema adalah bahasa kontrak untuk mendeskripsikan struktur dan constraint JSON data. Ia dapat dipakai untuk:
- API request/response schema;
- event payload schema;
- configuration schema;
- webhook contract;
- message envelope validation;
- schema registry artifact;
- form generation;
- documentation;
- policy validation;
- compatibility governance.
Namun JSON Schema juga punya banyak jebakan:
requiredsering disalahpahami;nullableberbeda antara OpenAPI 3.0 dan JSON Schema;additionalPropertiessering diabaikan;oneOf/anyOf/allOfsering dipakai salah;$id,$ref, dan$defssering membingungkan;- format validation sering tidak seketat yang dikira;
- schema validation sering dicampur dengan business validation;
- schema compatibility tidak otomatis berarti semantic compatibility.
Setelah part ini, kamu harus mampu:
- membaca JSON Schema sebagai contract;
- mendesain schema object, array, string, number, enum, const, composition;
- memahami
required, null, absent, default, and additional properties; - memakai
$id,$ref,$defs, anchors, dan modularization; - membedakan assertion, annotation, and applicator keywords;
- menentukan validation boundary antara syntax/shape/business rule;
- menyelaraskan JSON Schema dengan OpenAPI;
- memakai JSON Schema di Java runtime dan CI;
- menilai schema changes dari sisi compatibility;
- menghindari schema yang valid tetapi buruk sebagai contract.
1. JSON Schema Mental Model
JSON Schema adalah schema untuk JSON values.
JSON value bisa berupa:
- object;
- array;
- string;
- number;
- integer;
- boolean;
- null.
Schema bukan Java class. Schema bukan DTO. Schema adalah kontrak data.
Important:
Valid under schema does not necessarily mean valid under business rules.
Example:
{
"birthDate": "2030-01-01"
}
Schema may validate it as format: date, but business may reject future birth date.
2. Dialects and $schema
JSON Schema has dialects/versions.
Modern contract should explicitly declare dialect:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.acme.com/customer/CustomerRegistered.schema.json",
"type": "object"
}
Why $schema matters:
- validator knows keyword semantics;
- draft differences are real;
- OpenAPI tooling may support subset/variant;
- compatibility/diff tooling needs stable interpretation.
Do not assume every validator supports Draft 2020-12. Tooling maturity matters.
3. Core Vocabulary
| Concept | Meaning |
|---|---|
| instance | JSON data being validated |
| schema | JSON object/boolean defining validation rules |
| dialect | version/vocabulary of JSON Schema |
| keyword | schema property with special meaning |
| assertion | keyword that can make validation fail |
| annotation | keyword that adds metadata |
| applicator | keyword that applies subschemas |
| subschema | nested schema |
| reference | $ref to another schema/subschema |
| vocabulary | group of keywords with semantics |
Example:
{
"type": "string",
"minLength": 1,
"description": "Customer display name."
}
type,minLengthare assertions.descriptionis annotation.
4. Boolean Schemas
JSON Schema can be boolean.
true
means accepts everything.
false
means accepts nothing.
Useful in composition and advanced schemas, but avoid in business contracts unless clear.
Example:
{
"type": "object",
"properties": {
"legacyField": false
}
}
This rejects legacyField if present.
5. Object Schema Basics
{
"type": "object",
"required": ["customerId", "displayName"],
"properties": {
"customerId": {
"type": "string"
},
"displayName": {
"type": "string",
"minLength": 1,
"maxLength": 200
}
},
"additionalProperties": false
}
Key points:
| Keyword | Meaning |
|---|---|
type: object | instance must be object |
properties | schemas for named properties |
required | property names that must be present |
additionalProperties | schema/boolean for properties not listed |
propertyNames | validates property names |
minProperties / maxProperties | object size constraints |
dependentRequired | if one property exists, require others |
dependentSchemas | if property exists, apply schema |
6. Required vs Nullable vs Absent
Critical concept.
Schema:
{
"type": "object",
"required": ["displayName"],
"properties": {
"displayName": {
"type": "string"
},
"middleName": {
"type": ["string", "null"]
}
}
}
Meaning:
| Field | Required? | Null allowed? |
|---|---|---|
displayName | yes | no |
middleName | no | yes if present |
| unknown fields | depends on additionalProperties | depends |
Important:
requiredmeans property must exist.type: ["string", "null"]means value may be string or null.- A field can be required and nullable.
- A field can be optional and non-nullable.
- Absent and null are different.
6.1 Required Nullable
{
"type": "object",
"required": ["middleName"],
"properties": {
"middleName": {
"type": ["string", "null"]
}
}
}
Valid:
{ "middleName": null }
Invalid:
{}
6.2 Optional Non-Nullable
{
"type": "object",
"properties": {
"middleName": {
"type": "string"
}
}
}
Valid:
{}
Valid:
{ "middleName": "Sari" }
Invalid:
{ "middleName": null }
7. default Is Annotation
In JSON Schema, default is annotation. It does not mean validator will insert the value.
Example:
{
"type": "object",
"properties": {
"pageSize": {
"type": "integer",
"default": 50
}
}
}
A standard validator generally validates:
{}
as valid, but does not mutate it into:
{ "pageSize": 50 }
Defaulting is application behavior.
Contract must say:
- is field optional?
- if absent, does provider default?
- what is default?
- is default stable?
- does defaulting happen before business validation?
Do not rely on JSON Schema default alone for runtime behavior.
8. Additional Properties
8.1 Strict Object
{
"type": "object",
"properties": {
"customerId": { "type": "string" }
},
"additionalProperties": false
}
Rejects:
{
"customerId": "cus_123",
"typoField": "x"
}
8.2 Open Object
If omitted, additional properties are generally allowed.
{
"type": "object",
"properties": {
"customerId": { "type": "string" }
}
}
This accepts unknown fields.
8.3 Typed Additional Properties
{
"type": "object",
"additionalProperties": {
"type": "string"
}
}
Useful for labels/metadata maps.
8.4 Contract Policy
Recommended:
| Context | Policy |
|---|---|
| command/request payload | usually additionalProperties: false |
| response payload | tolerate consumer ignoring unknown fields; schema can still document strict known structure |
| metadata map | typed additionalProperties |
| event envelope | strict metadata fields plus explicit extension area |
| external partner API | be explicit; no accidental open objects |
If you allow extensions:
{
"type": "object",
"properties": {
"customerId": { "type": "string" },
"extensions": {
"type": "object",
"additionalProperties": true
}
},
"additionalProperties": false
}
This prevents random top-level leakage while allowing controlled extension.
9. String Constraints
{
"type": "string",
"minLength": 1,
"maxLength": 200,
"pattern": "^[A-Za-z0-9_-]+$"
}
Contract cautions:
- regex dialect is ECMA-262-like in JSON Schema context; validate tooling behavior;
minLengthcounts characters, but Unicode handling may surprise;formatis not always assertion;- tightening maxLength is breaking;
- changing pattern is often breaking;
- empty string and null are different.
9.1 ID Field
{
"type": "string",
"pattern": "^cus_[A-HJ-KM-NP-TV-Z0-9]{26}$",
"description": "Stable customer identifier."
}
Be careful: if you constrain ID format too tightly, future ID migration becomes breaking.
10. Number and Integer Constraints
{
"type": "integer",
"minimum": 0,
"maximum": 1000
}
Keywords:
| Keyword | Meaning |
|---|---|
minimum | inclusive min |
exclusiveMinimum | exclusive min |
maximum | inclusive max |
exclusiveMaximum | exclusive max |
multipleOf | numeric divisibility |
10.1 Money Warning
Do not model money as JSON number casually.
Bad:
{
"amount": {
"type": "number"
}
}
Potential issues:
- floating point in consumers;
- precision loss;
- currency missing;
- scale undefined;
- rounding ambiguous.
Prefer:
{
"$defs": {
"Money": {
"type": "object",
"required": ["currency", "value"],
"properties": {
"currency": {
"type": "string",
"minLength": 3,
"maxLength": 3
},
"value": {
"type": "string",
"pattern": "^-?[0-9]+(\\.[0-9]+)?$"
}
},
"additionalProperties": false
}
}
}
11. Array Constraints
{
"type": "array",
"items": {
"type": "string"
},
"minItems": 1,
"maxItems": 100,
"uniqueItems": true
}
Contract questions:
- Is order meaningful?
- Are duplicates allowed?
- Is empty array different from absent?
- Is array size bounded?
- Does order have stable semantics?
- Are items nullable?
11.1 Tuple-Like Arrays
Draft 2020-12 uses prefixItems for tuple validation.
{
"type": "array",
"prefixItems": [
{ "type": "number" },
{ "type": "number" }
],
"items": false,
"minItems": 2,
"maxItems": 2
}
For business contracts, prefer objects over tuple arrays unless there is strong reason.
Bad:
[106.8, -6.2]
Better:
{
"longitude": 106.8,
"latitude": -6.2
}
12. Enum and Const
12.1 Enum
{
"type": "string",
"enum": ["PENDING", "APPROVED", "REJECTED"]
}
Closed set.
Adding enum value can be dangerous for old consumers.
12.2 Const
{
"const": "CaseApproved"
}
Useful for event type discriminator.
Example:
{
"type": "object",
"required": ["eventType"],
"properties": {
"eventType": {
"const": "CaseApproved"
}
}
}
12.3 Open Enum Pattern
If taxonomy evolves:
{
"type": "string",
"description": "Known values: LOW, MEDIUM, HIGH. Consumers must tolerate unknown values."
}
JSON Schema cannot express “known but open enum” as a standard assertion without rejecting unknown values. Use documentation, examples, lint rules, and consumer tests.
13. Composition: allOf, anyOf, oneOf, not
13.1 allOf
All schemas must validate.
{
"allOf": [
{ "$ref": "#/$defs/EventEnvelopeBase" },
{
"type": "object",
"properties": {
"payload": { "$ref": "#/$defs/CaseApprovedPayload" }
}
}
]
}
Common misconception: allOf merges objects. Technically it applies multiple schemas. Tooling may display as inheritance, but validation is applicative.
13.2 anyOf
At least one schema must validate.
{
"anyOf": [
{ "$ref": "#/$defs/EmailContact" },
{ "$ref": "#/$defs/PhoneContact" }
]
}
Use when multiple schemas may overlap.
13.3 oneOf
Exactly one schema must validate.
{
"oneOf": [
{ "$ref": "#/$defs/DocumentVerification" },
{ "$ref": "#/$defs/BiometricVerification" }
]
}
Pitfall: if instance validates against two schemas, oneOf fails.
For discriminated unions, make schemas mutually exclusive with const discriminator.
13.4 not
Reject matching schema.
{
"not": {
"required": ["legacyField"]
}
}
Use sparingly.
14. Discriminator Pattern
JSON Schema itself can model discriminated variants using oneOf + const.
{
"oneOf": [
{ "$ref": "#/$defs/DocumentVerification" },
{ "$ref": "#/$defs/BiometricVerification" }
],
"$defs": {
"DocumentVerification": {
"type": "object",
"required": ["type", "documentNumber"],
"properties": {
"type": { "const": "DOCUMENT" },
"documentNumber": { "type": "string" }
},
"additionalProperties": false
},
"BiometricVerification": {
"type": "object",
"required": ["type", "biometricSessionId"],
"properties": {
"type": { "const": "BIOMETRIC" },
"biometricSessionId": { "type": "string" }
},
"additionalProperties": false
}
}
}
OpenAPI has discriminator support, but JSON Schema and OpenAPI tool support differs. Test with your generator/validator.
15. Conditional Schemas
Keywords:
if;then;else;dependentRequired;dependentSchemas.
Example:
{
"type": "object",
"properties": {
"accountType": {
"type": "string",
"enum": ["BASIC", "PREMIUM"]
},
"incomeProofDocumentId": {
"type": "string"
}
},
"if": {
"properties": {
"accountType": { "const": "PREMIUM" }
},
"required": ["accountType"]
},
"then": {
"required": ["incomeProofDocumentId"]
}
}
This is structural/semantic-ish validation, but be careful not to encode complex business policy into JSON Schema if it changes often or requires external state.
16. $id, $ref, $defs
16.1 $id
$id identifies schema URI/base.
{
"$id": "https://schemas.acme.com/common/Money.schema.json",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object"
}
Use stable, versioned or governance-controlled IDs.
16.2 $defs
Reusable subschemas.
{
"$defs": {
"Money": {
"type": "object",
"required": ["currency", "value"],
"properties": {
"currency": { "type": "string" },
"value": { "type": "string" }
}
}
}
}
16.3 $ref
Reference schema/subschema.
{
"$ref": "#/$defs/Money"
}
External:
{
"$ref": "https://schemas.acme.com/common/Money.schema.json"
}
16.4 Reference Design Rules
- use stable IDs;
- avoid circular references unless validator/tooling supports;
- bundle schemas for CI/publishing if external refs are difficult;
- version common schemas intentionally;
- do not change common schema semantics without impact analysis.
17. Modular Schema Repository
Example:
schemas/
├── common/
│ ├── Money.schema.json
│ ├── EventMetadata.schema.json
│ └── Problem.schema.json
├── customer/
│ ├── CustomerRegistered.schema.json
│ └── CustomerSnapshotUpdated.schema.json
├── case/
│ ├── CaseApproved.schema.json
│ └── CaseSubmitted.schema.json
└── bundle/
└── case-events.bundle.schema.json
Use bundling to make validation deterministic.
Repository metadata:
schemas/catalog.yaml
schemas/owners.yaml
schemas/compatibility.yaml
schemas/deprecations.yaml
18. Format Keyword
format is annotation by default in some dialect/tooling unless format assertion is enabled.
Example:
{
"type": "string",
"format": "email"
}
Do not assume every validator rejects invalid emails unless configured.
Common formats:
date;date-time;time;duration;email;hostname;ipv4;ipv6;uuid;uri;uri-reference.
Contract rule:
If format must be enforced, configure validator and test it.
For critical fields, combine format with pattern or application validation if necessary.
19. Unevaluated Properties
Modern JSON Schema includes keywords such as unevaluatedProperties, useful with composition.
Example:
{
"allOf": [
{ "$ref": "#/$defs/BaseEvent" },
{ "$ref": "#/$defs/CaseApprovedPayloadWrapper" }
],
"unevaluatedProperties": false
}
This can help avoid additionalProperties problems with allOf.
But tool support and complexity vary. Use carefully and test.
20. Validation Boundary
Do not put all validation into JSON Schema.
20.1 Good for JSON Schema
- JSON type;
- required fields;
- string length;
- regex shape;
- numeric bounds;
- array size;
- object structure;
- enum/const when closed;
- simple conditional structure;
- additional properties policy.
20.2 Better for Application/Domain Validation
- customer exists;
- case is in correct state;
- user has entitlement;
- KYC verified;
- balance sufficient;
- birth date age rule by jurisdiction;
- policy rule active;
- uniqueness constraint;
- cross-service reference validation;
- time-window based business rules.
20.3 Boundary Diagram
Schema validation is an early boundary, not whole business truth.
21. JSON Schema and OpenAPI Alignment
OpenAPI 3.1 aligns much more closely with JSON Schema Draft 2020-12 than OpenAPI 3.0 did.
21.1 OpenAPI 3.0 Nullable
type: string
nullable: true
21.2 OpenAPI 3.1 / JSON Schema Style
type:
- string
- "null"
or JSON:
{
"type": ["string", "null"]
}
21.3 Contract Risk
If your tooling mixes:
- OpenAPI 3.0;
- OpenAPI 3.1;
- JSON Schema Draft 7;
- JSON Schema Draft 2020-12;
then nullability, composition, and validation behavior can drift.
Policy:
- define supported dialect/version;
- test validators/generators;
- avoid keywords unsupported by target tools;
- document limitations;
- do not assume OpenAPI schema is arbitrary JSON Schema unless version supports it.
22. Java Validation Libraries
In Java, JSON Schema validation libraries vary in draft support and performance.
Contract engineering concerns:
- which draft is supported?
- is
formatasserted? - are external refs resolved?
- is schema compiled/cached?
- are validation errors machine-readable?
- performance under large payloads?
- streaming validation needed?
- thread safety?
- integration with Jackson?
- error path format?
Pseudo usage:
public final class JsonSchemaValidator {
private final SchemaRegistry schemaRegistry;
public ValidationResult validate(String schemaId, JsonNode payload) {
JsonSchema schema = schemaRegistry.compiledSchema(schemaId);
Set<ValidationMessage> messages = schema.validate(payload);
return ValidationResult.from(messages);
}
}
Do not compile schema per request. Cache compiled schemas.
23. Validation Error Mapping
JSON Schema validator errors must map to stable API/event error contract.
Raw validator message:
$.birthDate: does not match format date
Canonical violation:
{
"field": "/birthDate",
"code": "INVALID_DATE_FORMAT",
"message": "birthDate must be an ISO-8601 date."
}
Rules:
- do not expose validator internals directly;
- convert paths to JSON Pointer consistently;
- map keyword failures to stable violation codes;
- avoid leaking sensitive rejected values;
- aggregate useful errors;
- preserve correlation/event ID.
Keyword-to-code example:
| JSON Schema keyword | Violation code |
|---|---|
required | REQUIRED |
type | INVALID_TYPE |
format | INVALID_FORMAT |
minLength | TOO_SHORT |
maxLength | TOO_LONG |
pattern | PATTERN_MISMATCH |
enum | UNSUPPORTED_VALUE |
additionalProperties | UNKNOWN_FIELD |
minimum | BELOW_MINIMUM |
maximum | ABOVE_MAXIMUM |
oneOf | INVALID_VARIANT |
24. Runtime Validation for Events
Producer-side:
schemaValidator.validate("CaseApproved", eventJson);
publisher.publish(eventJson);
Consumer-side:
if (!schemaValidator.isValid("CaseApproved", eventJson)) {
quarantine(eventJson, "SCHEMA_VALIDATION_FAILED");
return;
}
handler.handle(eventJson);
Guidance:
| Location | Purpose |
|---|---|
| producer | prevent invalid events from entering stream |
| registry | prevent incompatible schema publication |
| consumer | protect consumer from bad/legacy/poison data |
| CI | prevent schema drift |
| staging | catch integration mismatch |
| production | validate selectively or at boundary depending risk/performance |
For high-volume Kafka, full runtime validation can be expensive. Balance with serializer validation, schema registry, and sampling.
25. Schema Compatibility for JSON Schema
JSON Schema compatibility is harder than Avro/Protobuf because JSON Schema is very expressive and semantic comparison can be complex.
Practical classification:
25.1 Often Backward-Compatible for Readers
- add optional property;
- widen allowed string length;
- widen numeric bounds;
- allow additional enum value only if consumers tolerate unknown;
- allow null where previously not allowed, if consumers can handle;
- relax pattern.
25.2 Often Breaking for Writers/Requests
- add required property;
- tighten maxLength;
- tighten pattern;
- remove allowed enum value;
- change type;
- disallow previously allowed additional properties;
- remove property expected by consumers;
- change nullability;
- change oneOf discriminator;
- change default semantics.
25.3 Direction Matters
For request schemas:
- Provider tightening schema breaks existing consumers.
- Provider loosening schema may be safe but might allow bad data.
For response/event schemas:
- Producer adding optional fields is usually safe if consumers ignore unknown.
- Producer removing fields breaks consumers that read them.
- Producer adding enum value may break consumers with closed enum logic.
26. JSON Schema Diff Is Not Enough
Schema diff can detect structural changes.
It may miss semantic changes:
riskScorerange meaning changed;statusvalue meaning changed;datetime zone semantics changed;amount.valuescale changed but still string;eventTypepublished at different lifecycle point;- field source authority changed;
- default behavior changed outside schema.
Schema review must include semantic notes and examples.
27. Event Envelope Schema Example
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.acme.com/events/CaseApproved.schema.json",
"title": "CaseApproved",
"type": "object",
"required": ["metadata", "payload"],
"properties": {
"metadata": {
"$ref": "https://schemas.acme.com/common/EventMetadata.schema.json"
},
"payload": {
"$ref": "#/$defs/CaseApprovedPayload"
}
},
"additionalProperties": false,
"$defs": {
"CaseApprovedPayload": {
"type": "object",
"required": [
"caseId",
"caseVersion",
"approvedBy",
"approvedAt",
"reasonCode"
],
"properties": {
"caseId": {
"type": "string",
"pattern": "^case_[A-Za-z0-9]+$"
},
"caseVersion": {
"type": "integer",
"minimum": 1
},
"approvedBy": {
"type": "string"
},
"approvedAt": {
"type": "string",
"format": "date-time"
},
"reasonCode": {
"type": "string"
}
},
"additionalProperties": false
}
}
}
Potential improvement: enforce metadata.eventType = CaseApproved with nested constraint if common metadata allows it.
28. Common Metadata Schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.acme.com/common/EventMetadata.schema.json",
"title": "EventMetadata",
"type": "object",
"required": [
"eventId",
"eventType",
"source",
"occurredAt",
"schemaRef"
],
"properties": {
"eventId": {
"type": "string",
"pattern": "^evt_[A-Za-z0-9]+$"
},
"eventType": {
"type": "string",
"minLength": 1
},
"eventVersion": {
"type": "string"
},
"source": {
"type": "string"
},
"aggregateType": {
"type": "string"
},
"aggregateId": {
"type": "string"
},
"aggregateVersion": {
"type": "integer",
"minimum": 1
},
"occurredAt": {
"type": "string",
"format": "date-time"
},
"publishedAt": {
"type": "string",
"format": "date-time"
},
"correlationId": {
"type": "string"
},
"causationId": {
"type": "string"
},
"schemaRef": {
"type": "string"
},
"tenantId": {
"type": "string"
},
"jurisdiction": {
"type": "string"
},
"dataClassification": {
"type": "string",
"enum": ["PUBLIC", "INTERNAL", "CONFIDENTIAL", "RESTRICTED"]
}
},
"additionalProperties": false
}
Important: common metadata schema changes affect every event. Govern strongly.
29. JSON Schema Governance Policy
Example:
jsonSchemaPolicy:
dialect: "https://json-schema.org/draft/2020-12/schema"
requireSchemaId: true
requireAdditionalPropertiesPolicy: true
requireExamples: true
formatAssertion: enabled-in-ci
commonSchemas:
- EventMetadata
- Money
- Problem
strictRequests:
additionalProperties: false
eventSchemas:
requireEventTypeConst: true
requireMetadataRef: true
requireSchemaRef: true
compatibility:
addedRequiredField: breaking
removedProperty: breaking
typeChange: breaking
enumValueAdded: dangerous
constraintTightening: dangerous-or-breaking
validationErrors:
useStableViolationCodes: true
30. Review Checklist
30.1 Dialect and Identity
- Is
$schemapresent? - Is
$idstable? - Are external refs resolvable?
- Is schema versioning strategy clear?
30.2 Object Design
- Is
requiredcorrect? - Are nullable fields explicit?
- Is
additionalPropertiesexplicit? - Are extension points intentional?
- Are defaults documented as application behavior?
30.3 Types and Constraints
- Are money/time/ID fields modeled safely?
- Are string patterns too strict?
- Are numeric ranges future-proof?
- Are arrays bounded?
- Is enum closed intentionally?
- Is
formatenforcement tested?
30.4 Composition
- Is
oneOfmutually exclusive? - Is discriminator explicit?
- Is
allOfnot mistaken as inheritance? - Is
unevaluatedPropertiesused only if supported?
30.5 Validation Boundary
- Is schema validation separated from business validation?
- Are error codes stable?
- Are sensitive rejected values hidden?
- Are validator-specific messages normalized?
30.6 Compatibility
- Did schema add required property?
- Did schema remove property?
- Did constraints tighten?
- Did enum change?
- Did nullability change?
- Did semantic meaning change outside schema?
31. Anti-Patterns
31.1 No $schema
Validator uses unknown/default dialect.
31.2 Open Object Accidentally
No additionalProperties policy.
31.3 Required Everything
Makes evolution hard.
31.4 Nullable Everything
Hides semantics and pushes complexity to consumers.
31.5 Enum for Rapidly Changing Taxonomy
Old consumers break or reject future values.
31.6 Business Rules in Huge Conditional Schema
Schema becomes unmaintainable and environment-dependent.
31.7 Format Assumed But Not Enforced
format: email present but validator not configured.
31.8 oneOf Without Discriminator
Ambiguous validation failures.
31.9 Common Schema Changed Casually
Every event/API breaks.
31.10 Default Misunderstood
Expecting validator to fill values.
31.11 Pattern Too Strict
Future ID migration becomes breaking.
31.12 Schema Diff Treated as Semantic Review
Meaning changes undetected.
32. Practice Lab
Lab 1 — Required vs Null
Design schema for:
displayNamerequired non-null string;middleNameoptional nullable string;emailAddressoptional non-null email string;birthDaterequired date string.
Explain valid/invalid examples.
Lab 2 — Event Schema
Create JSON Schema for CustomerActivated envelope with metadata and payload.
Enforce:
metadata.eventTypeisCustomerActivated;payload.customerIdrequired;payload.previousLifecycleStatusrequired;payload.newLifecycleStatusrequired;- no additional payload properties.
Lab 3 — oneOf Discriminator
Model verification method:
DOCUMENT;BIOMETRIC;MANUAL_REVIEW.
Use oneOf + const.
Lab 4 — Compatibility Classification
Classify:
- add optional property;
- add required property;
- remove property;
- widen maxLength;
- tighten maxLength;
- add enum value;
- remove enum value;
- allow null;
- disallow null;
- add additionalProperties false;
- change format from date to date-time;
- change description only.
Lab 5 — Java Error Mapping
Map validator errors for:
- missing field;
- invalid type;
- unknown field;
- enum mismatch;
- pattern mismatch;
to stable API problem violations.
33. Senior Engineer Heuristics
- JSON Schema validates shape, not full business truth.
- Always declare
$schema. requiredis about presence, not non-null.- Null and absent are different contract states.
defaultis annotation, not mutation.- Be explicit about
additionalProperties. formatmay not assert unless validator enables it.- Use
oneOfonly with mutually exclusive schemas. - Schema composition is validation, not object-oriented inheritance.
- Open enum requires documentation and consumer tests, not just schema.
- Common schemas are high-blast-radius assets.
- Tightening constraints is often breaking.
- Schema compatibility direction depends on request vs response/event.
- Normalize validator errors into stable contract errors.
- JSON Schema diff cannot replace semantic review.
34. Summary
JSON Schema is a powerful contract language for JSON payloads, but it requires precision. The most important concepts are dialect, $id, $ref, $defs, required vs null, additional properties, composition, validation boundaries, and compatibility direction.
Main takeaways:
- JSON Schema is not Java DTO validation;
- Draft/dialect matters;
required, null, absent, and default have distinct meanings;additionalPropertiesis a core governance decision;oneOf,anyOf,allOf, andnotneed careful semantics;$id,$ref, and$defsenable modular schemas but require stable references;formatenforcement must be tested;- business rules should not all be forced into schema;
- OpenAPI alignment depends on OpenAPI version;
- schema compatibility is not semantic compatibility.
Part berikutnya membahas event versioning and compatibility: additive evolution, semantic breaks, dual publish, upcasting, translation topics, consumer lag, replay compatibility, and migration patterns.
You just completed lesson 19 in deepen practice. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.