@@ -254,10 +254,11 @@ type extractor struct {
254254 done map [string ]bool
255255
256256 // per package
257- pkg * packages.Package
258- orig map [types.Type ]* ast.StructType
259- usedPkgs map [string ]bool
260- consts map [string ][]string
257+ pkg * packages.Package
258+ orig map [types.Type ]* ast.StructType
259+ usedPkgs map [string ]bool
260+ consts map [string ][]string
261+ k8sSemantic bool
261262
262263 // per file
263264 cmap ast.CommentMap
@@ -446,7 +447,16 @@ func (e *extractor) recordTypeInfo(p *packages.Package) {
446447
447448func (e * extractor ) extractPkg (root string , p * packages.Package ) error {
448449 e .pkg = p
449- e .logf ("--- Package %s" , p .PkgPath )
450+ e .k8sSemantic = false
451+
452+ for _ , f := range p .Syntax {
453+ for _ , c := range f .Comments {
454+ if strings .Contains (c .Text (), "+k8s:openapi-gen=true" ) {
455+ e .k8sSemantic = true
456+ }
457+ }
458+ }
459+ e .logf ("--- Package %s - Kubernetes semantics: %t" , p .PkgPath , e .k8sSemantic )
450460
451461 e .recordTypeInfo (p )
452462
@@ -857,7 +867,7 @@ func (e *extractor) reportDecl(x *ast.GenDecl) (a []cueast.Decl) {
857867 if basic , ok := typ .(* types.Basic ); ok && basic .Info ()& types .IsUntyped != 0 {
858868 break // untyped basic types do not make valid identifiers
859869 }
860- cv = cueast .NewBinExpr (cuetoken .AND , e .makeType (typ ), cv )
870+ cv = cueast .NewBinExpr (cuetoken .AND , e .makeType (typ , regular ), cv )
861871 }
862872
863873 f .Value = cv
@@ -1047,7 +1057,7 @@ const (
10471057)
10481058
10491059func (e * extractor ) makeField (name string , kind fieldKind , expr types.Type , doc * ast.CommentGroup , newline bool ) (f * cueast.Field , typename string ) {
1050- typ := e .makeType (expr )
1060+ typ := e .makeType (expr , kind )
10511061 var label cueast.Label
10521062 if kind == definition {
10531063 label = e .ident (name , true )
@@ -1075,7 +1085,7 @@ func (e *extractor) makeField(name string, kind fieldKind, expr types.Type, doc
10751085 return f , string (b )
10761086}
10771087
1078- func (e * extractor ) makeType (typ types.Type ) (result cueast.Expr ) {
1088+ func (e * extractor ) makeType (typ types.Type , kind fieldKind ) (result cueast.Expr ) {
10791089 switch typ := types .Unalias (typ ).(type ) {
10801090 case * types.Named :
10811091 obj := typ .Obj ()
@@ -1209,10 +1219,14 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12091219
12101220 return result
12111221 case * types.Pointer :
1222+ // In K8s, a field is only nullable if explicitly marked as such.
1223+ if e .k8sSemantic && kind == optional {
1224+ return e .makeType (typ .Elem (), kind )
1225+ }
12121226 return & cueast.BinaryExpr {
12131227 X : cueast .NewNull (),
12141228 Op : cuetoken .OR ,
1215- Y : e .makeType (typ .Elem ()),
1229+ Y : e .makeType (typ .Elem (), kind ),
12161230 }
12171231
12181232 case * types.Struct :
@@ -1231,7 +1245,7 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12311245 if typ .Elem () == typeByte {
12321246 return e .ident ("bytes" , false )
12331247 }
1234- return cueast .NewList (& cueast.Ellipsis {Type : e .makeType (typ .Elem ())})
1248+ return cueast .NewList (& cueast.Ellipsis {Type : e .makeType (typ .Elem (), kind )})
12351249
12361250 case * types.Array :
12371251 if typ .Elem () == typeByte {
@@ -1248,7 +1262,7 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12481262 Value : strconv .Itoa (int (typ .Len ())),
12491263 },
12501264 Op : cuetoken .MUL ,
1251- Y : cueast .NewList (e .makeType (typ .Elem ())),
1265+ Y : cueast .NewList (e .makeType (typ .Elem (), kind )),
12521266 }
12531267 }
12541268
@@ -1259,7 +1273,7 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12591273
12601274 f := & cueast.Field {
12611275 Label : cueast .NewList (e .ident ("string" , false )),
1262- Value : e .makeType (typ .Elem ()),
1276+ Value : e .makeType (typ .Elem (), kind ),
12631277 }
12641278 cueast .SetRelPos (f , cuetoken .Blank )
12651279 return & cueast.StructLit {
@@ -1282,7 +1296,7 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12821296 case * types.Union :
12831297 var exprs []cueast.Expr
12841298 for term := range typ .Terms () {
1285- exprs = append (exprs , e .makeType (term .Type ()))
1299+ exprs = append (exprs , e .makeType (term .Type (), kind ))
12861300 }
12871301 return cueast .NewBinExpr (cuetoken .OR , exprs ... )
12881302
@@ -1305,12 +1319,12 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
13051319 //
13061320 var exprs []cueast.Expr
13071321 for etyp := range typ .EmbeddedTypes () {
1308- exprs = append (exprs , e .makeType (etyp ))
1322+ exprs = append (exprs , e .makeType (etyp , kind ))
13091323 }
13101324 return cueast .NewBinExpr (cuetoken .OR , exprs ... )
13111325
13121326 case * types.TypeParam :
1313- return e .makeType (typ .Constraint ())
1327+ return e .makeType (typ .Constraint (), kind )
13141328
13151329 default :
13161330 // record error
@@ -1360,7 +1374,7 @@ func (e *extractor) addFields(x *types.Struct, st *cueast.StructLit) {
13601374 }
13611375 switch typ := types .Unalias (typ ).(type ) {
13621376 case * types.Named :
1363- embed := & cueast.EmbedDecl {Expr : e .makeType (typ )}
1377+ embed := & cueast.EmbedDecl {Expr : e .makeType (typ , regular )}
13641378 if i > 0 {
13651379 cueast .SetRelPos (embed , cuetoken .NewSection )
13661380 }
@@ -1381,10 +1395,7 @@ func (e *extractor) addFields(x *types.Struct, st *cueast.StructLit) {
13811395 doc := docs [i ]
13821396
13831397 // TODO: check referrers
1384- kind := regular
1385- if e .isOptional (f , doc , tag ) {
1386- kind = optional
1387- }
1398+ kind := e .detectFieldKind (f , doc , tag )
13881399 field , cueType := e .makeField (name , kind , f .Type (), doc , count > 0 )
13891400 add (field )
13901401
@@ -1480,24 +1491,36 @@ func (e *extractor) isInline(tag string) bool {
14801491 hasFlag (tag , "yaml" , "inline" , 1 )
14811492}
14821493
1483- func (e * extractor ) isOptional (f * types.Var , doc * ast.CommentGroup , tag string ) bool {
1484- if _ , ok := f .Type ().(* types.Pointer ); ok {
1485- return true
1486- }
1487-
1494+ func (e * extractor ) detectFieldKind (f * types.Var , doc * ast.CommentGroup , tag string ) fieldKind {
1495+ // From k8s docs:
1496+ // - a map field marked with `+nullable` would accept either `foo: null` or `foo: {}`.
1497+ // - Using the `+optional` or the `omitempty` tag means that the field is optional.
1498+ // Note that for backward compatibility, any field that has the `omitempty` struct
1499+ // tag, and is not explicitly marked as `+required`, will be considered to be optional.
14881500 for line := range strings .SplitSeq (doc .Text (), "\n " ) {
14891501 before , _ , _ := strings .Cut (strings .TrimSpace (line ), "=" )
1490- if before == "+optional" {
1491- return true
1502+ switch before {
1503+ case "+nullable" :
1504+ return regular
1505+ case "+optional" :
1506+ return optional
14921507 }
14931508 }
14941509
1510+ if _ , ok := f .Type ().(* types.Pointer ); ok {
1511+ return optional
1512+ }
1513+
14951514 // Go 1.24 added the "omitzero" option to encoding/json, an improvement over "omitempty".
14961515 // Note that, as of mid 2025, YAML libraries don't seem to have picked up "omitzero" yet.
14971516 // TODO: also when the type is a list or other kind of pointer.
1498- return hasFlag (tag , "json" , "omitempty" , 1 ) ||
1517+ if hasFlag (tag , "json" , "omitempty" , 1 ) ||
14991518 hasFlag (tag , "json" , "omitzero" , 1 ) ||
1500- hasFlag (tag , "yaml" , "omitempty" , 1 )
1519+ hasFlag (tag , "yaml" , "omitempty" , 1 ) {
1520+ return optional
1521+ }
1522+
1523+ return regular
15011524}
15021525
15031526func hasFlag (tag , key , flag string , offset int ) bool {
0 commit comments