@@ -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
@@ -1043,11 +1053,12 @@ type fieldKind int
10431053const (
10441054 regular fieldKind = iota
10451055 optional
1056+ nullable
10461057 definition
10471058)
10481059
10491060func (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 )
1061+ typ := e .makeType (expr , kind )
10511062 var label cueast.Label
10521063 if kind == definition {
10531064 label = e .ident (name , true )
@@ -1075,7 +1086,7 @@ func (e *extractor) makeField(name string, kind fieldKind, expr types.Type, doc
10751086 return f , string (b )
10761087}
10771088
1078- func (e * extractor ) makeType (typ types.Type ) (result cueast.Expr ) {
1089+ func (e * extractor ) makeType (typ types.Type , kind fieldKind ) (result cueast.Expr ) {
10791090 switch typ := types .Unalias (typ ).(type ) {
10801091 case * types.Named :
10811092 obj := typ .Obj ()
@@ -1209,10 +1220,14 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12091220
12101221 return result
12111222 case * types.Pointer :
1223+ // In K8s, a field is only nullable if explicitly marked as such.
1224+ if e .k8sSemantic && kind == optional {
1225+ return e .makeType (typ .Elem (), kind )
1226+ }
12121227 return & cueast.BinaryExpr {
12131228 X : cueast .NewNull (),
12141229 Op : cuetoken .OR ,
1215- Y : e .makeType (typ .Elem ()),
1230+ Y : e .makeType (typ .Elem (), kind ),
12161231 }
12171232
12181233 case * types.Struct :
@@ -1231,7 +1246,7 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12311246 if typ .Elem () == typeByte {
12321247 return e .ident ("bytes" , false )
12331248 }
1234- return cueast .NewList (& cueast.Ellipsis {Type : e .makeType (typ .Elem ())})
1249+ return cueast .NewList (& cueast.Ellipsis {Type : e .makeType (typ .Elem (), kind )})
12351250
12361251 case * types.Array :
12371252 if typ .Elem () == typeByte {
@@ -1248,7 +1263,7 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12481263 Value : strconv .Itoa (int (typ .Len ())),
12491264 },
12501265 Op : cuetoken .MUL ,
1251- Y : cueast .NewList (e .makeType (typ .Elem ())),
1266+ Y : cueast .NewList (e .makeType (typ .Elem (), kind )),
12521267 }
12531268 }
12541269
@@ -1259,7 +1274,7 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12591274
12601275 f := & cueast.Field {
12611276 Label : cueast .NewList (e .ident ("string" , false )),
1262- Value : e .makeType (typ .Elem ()),
1277+ Value : e .makeType (typ .Elem (), kind ),
12631278 }
12641279 cueast .SetRelPos (f , cuetoken .Blank )
12651280 return & cueast.StructLit {
@@ -1282,7 +1297,7 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12821297 case * types.Union :
12831298 var exprs []cueast.Expr
12841299 for term := range typ .Terms () {
1285- exprs = append (exprs , e .makeType (term .Type ()))
1300+ exprs = append (exprs , e .makeType (term .Type (), kind ))
12861301 }
12871302 return cueast .NewBinExpr (cuetoken .OR , exprs ... )
12881303
@@ -1305,12 +1320,12 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
13051320 //
13061321 var exprs []cueast.Expr
13071322 for etyp := range typ .EmbeddedTypes () {
1308- exprs = append (exprs , e .makeType (etyp ))
1323+ exprs = append (exprs , e .makeType (etyp , kind ))
13091324 }
13101325 return cueast .NewBinExpr (cuetoken .OR , exprs ... )
13111326
13121327 case * types.TypeParam :
1313- return e .makeType (typ .Constraint ())
1328+ return e .makeType (typ .Constraint (), kind )
13141329
13151330 default :
13161331 // record error
@@ -1360,7 +1375,7 @@ func (e *extractor) addFields(x *types.Struct, st *cueast.StructLit) {
13601375 }
13611376 switch typ := types .Unalias (typ ).(type ) {
13621377 case * types.Named :
1363- embed := & cueast.EmbedDecl {Expr : e .makeType (typ )}
1378+ embed := & cueast.EmbedDecl {Expr : e .makeType (typ , regular )}
13641379 if i > 0 {
13651380 cueast .SetRelPos (embed , cuetoken .NewSection )
13661381 }
@@ -1381,10 +1396,7 @@ func (e *extractor) addFields(x *types.Struct, st *cueast.StructLit) {
13811396 doc := docs [i ]
13821397
13831398 // TODO: check referrers
1384- kind := regular
1385- if e .isOptional (f , doc , tag ) {
1386- kind = optional
1387- }
1399+ kind := e .detectFieldKind (f , doc , tag )
13881400 field , cueType := e .makeField (name , kind , f .Type (), doc , count > 0 )
13891401 add (field )
13901402
@@ -1480,24 +1492,36 @@ func (e *extractor) isInline(tag string) bool {
14801492 hasFlag (tag , "yaml" , "inline" , 1 )
14811493}
14821494
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-
1495+ func (e * extractor ) detectFieldKind (f * types.Var , doc * ast.CommentGroup , tag string ) fieldKind {
1496+ // From k8s docs:
1497+ // - a map field marked with `+nullable` would accept either `foo: null` or `foo: {}`.
1498+ // - Using the `+optional` or the `omitempty` tag means that the field is optional.
1499+ // Note that for backward compatibility, any field that has the `omitempty` struct
1500+ // tag, and is not explicitly marked as `+required`, will be considered to be optional.
14881501 for line := range strings .SplitSeq (doc .Text (), "\n " ) {
14891502 before , _ , _ := strings .Cut (strings .TrimSpace (line ), "=" )
1490- if before == "+optional" {
1491- return true
1503+ switch before {
1504+ case "+nullable" :
1505+ return nullable
1506+ case "+optional" :
1507+ return optional
14921508 }
14931509 }
14941510
1511+ if _ , ok := f .Type ().(* types.Pointer ); ok {
1512+ return optional
1513+ }
1514+
14951515 // Go 1.24 added the "omitzero" option to encoding/json, an improvement over "omitempty".
14961516 // Note that, as of mid 2025, YAML libraries don't seem to have picked up "omitzero" yet.
14971517 // TODO: also when the type is a list or other kind of pointer.
1498- return hasFlag (tag , "json" , "omitempty" , 1 ) ||
1518+ if hasFlag (tag , "json" , "omitempty" , 1 ) ||
14991519 hasFlag (tag , "json" , "omitzero" , 1 ) ||
1500- hasFlag (tag , "yaml" , "omitempty" , 1 )
1520+ hasFlag (tag , "yaml" , "omitempty" , 1 ) {
1521+ return optional
1522+ }
1523+
1524+ return regular
15011525}
15021526
15031527func hasFlag (tag , key , flag string , offset int ) bool {
0 commit comments