Skip to content

Commit 18a658d

Browse files
committed
Add nullables for OpenAPI and JsonSchema
1 parent 652b092 commit 18a658d

File tree

1 file changed

+49
-11
lines changed

1 file changed

+49
-11
lines changed

internal/jennies/jsonschema/schema.go

Lines changed: 49 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -111,30 +111,68 @@ func (jenny Schema) objectToDefinition(object ast.Object) Definition {
111111
}
112112

113113
func (jenny Schema) formatType(typeDef ast.Type) Definition {
114+
var definition Definition
114115
switch typeDef.Kind {
115116
case ast.KindStruct:
116-
return jenny.formatStruct(typeDef)
117+
definition = jenny.formatStruct(typeDef)
117118
case ast.KindScalar:
118-
return jenny.formatScalar(typeDef)
119+
definition = jenny.formatScalar(typeDef)
119120
case ast.KindRef:
120-
return jenny.formatRef(typeDef)
121+
definition = jenny.formatRef(typeDef)
121122
case ast.KindEnum:
122-
return jenny.formatEnum(typeDef)
123+
definition = jenny.formatEnum(typeDef)
123124
case ast.KindArray:
124-
return jenny.formatArray(typeDef)
125+
definition = jenny.formatArray(typeDef)
125126
case ast.KindMap:
126-
return jenny.formatMap(typeDef)
127+
definition = jenny.formatMap(typeDef)
127128
case ast.KindDisjunction:
128-
return jenny.formatDisjunction(typeDef)
129+
definition = jenny.formatDisjunction(typeDef)
129130
case ast.KindIntersection:
130-
return jenny.formatIntersection(typeDef)
131+
definition = jenny.formatIntersection(typeDef)
131132
case ast.KindComposableSlot:
132-
return jenny.formatComposableSlot()
133+
definition = jenny.formatComposableSlot()
133134
case ast.KindConstantRef:
134-
return jenny.formatConstantRef(typeDef)
135+
definition = jenny.formatConstantRef(typeDef)
136+
default:
137+
definition = orderedmap.New[string, any]()
135138
}
136139

137-
return orderedmap.New[string, any]()
140+
if typeDef.Nullable {
141+
definition = jenny.applyNullable(typeDef.Kind, definition)
142+
}
143+
144+
return definition
145+
}
146+
147+
// applyNullable wraps a definition to express "type | null".
148+
// Ref-like kinds need special treatment in OpenAPI 3.0 because $ref replaces the whole object
149+
// and cannot be combined with other keywords — those use allOf as a wrapper.
150+
// All other kinds can carry nullable: true inline (OpenAPI 3.0) or use anyOf (JSON Schema).
151+
func (jenny Schema) applyNullable(kind ast.Kind, definition Definition) Definition {
152+
nullDef := orderedmap.New[string, any]()
153+
nullDef.Set("type", "null")
154+
155+
refLike := kind == ast.KindRef || kind == ast.KindConstantRef
156+
157+
if jenny.OpenAPI3Compatible {
158+
nullable := orderedmap.New[string, any]()
159+
if refLike {
160+
nullable.Set("nullable", true)
161+
nullable.Set("allOf", []Definition{definition})
162+
} else {
163+
// Copy the existing definition and add nullable: true inline
164+
definition.Iterate(func(key string, value any) {
165+
nullable.Set(key, value)
166+
})
167+
nullable.Set("nullable", true)
168+
}
169+
return nullable
170+
}
171+
172+
// JSON Schema: anyOf: [{...}, {type: "null"}]
173+
wrapper := orderedmap.New[string, any]()
174+
wrapper.Set("anyOf", []Definition{definition, nullDef})
175+
return wrapper
138176
}
139177

140178
func (jenny Schema) formatScalar(typeDef ast.Type) Definition {

0 commit comments

Comments
 (0)