@@ -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,18 @@ 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+ outer:
453+ for _ , f := range p .Syntax {
454+ for _ , c := range f .Comments {
455+ if strings .Contains (c .Text (), "+k8s:openapi-gen=true" ) {
456+ e .k8sSemantic = true
457+ break outer
458+ }
459+ }
460+ }
461+ e .logf ("--- Package %s - Kubernetes semantics: %t" , p .PkgPath , e .k8sSemantic )
450462
451463 e .recordTypeInfo (p )
452464
@@ -857,7 +869,7 @@ func (e *extractor) reportDecl(x *ast.GenDecl) (a []cueast.Decl) {
857869 if basic , ok := typ .(* types.Basic ); ok && basic .Info ()& types .IsUntyped != 0 {
858870 break // untyped basic types do not make valid identifiers
859871 }
860- cv = cueast .NewBinExpr (cuetoken .AND , e .makeType (typ ), cv )
872+ cv = cueast .NewBinExpr (cuetoken .AND , e .makeType (typ , regular ), cv )
861873 }
862874
863875 f .Value = cv
@@ -1047,7 +1059,7 @@ const (
10471059)
10481060
10491061func (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 )
1062+ typ := e .makeType (expr , kind )
10511063 var label cueast.Label
10521064 if kind == definition {
10531065 label = e .ident (name , true )
@@ -1075,7 +1087,7 @@ func (e *extractor) makeField(name string, kind fieldKind, expr types.Type, doc
10751087 return f , string (b )
10761088}
10771089
1078- func (e * extractor ) makeType (typ types.Type ) (result cueast.Expr ) {
1090+ func (e * extractor ) makeType (typ types.Type , kind fieldKind ) (result cueast.Expr ) {
10791091 switch typ := types .Unalias (typ ).(type ) {
10801092 case * types.Named :
10811093 obj := typ .Obj ()
@@ -1209,10 +1221,14 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12091221
12101222 return result
12111223 case * types.Pointer :
1224+ // In K8s, a field is only nullable if explicitly marked as such.
1225+ if e .k8sSemantic && kind == optional {
1226+ return e .makeType (typ .Elem (), kind )
1227+ }
12121228 return & cueast.BinaryExpr {
12131229 X : cueast .NewNull (),
12141230 Op : cuetoken .OR ,
1215- Y : e .makeType (typ .Elem ()),
1231+ Y : e .makeType (typ .Elem (), kind ),
12161232 }
12171233
12181234 case * types.Struct :
@@ -1231,7 +1247,7 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12311247 if typ .Elem () == typeByte {
12321248 return e .ident ("bytes" , false )
12331249 }
1234- return cueast .NewList (& cueast.Ellipsis {Type : e .makeType (typ .Elem ())})
1250+ return cueast .NewList (& cueast.Ellipsis {Type : e .makeType (typ .Elem (), kind )})
12351251
12361252 case * types.Array :
12371253 if typ .Elem () == typeByte {
@@ -1248,7 +1264,7 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12481264 Value : strconv .Itoa (int (typ .Len ())),
12491265 },
12501266 Op : cuetoken .MUL ,
1251- Y : cueast .NewList (e .makeType (typ .Elem ())),
1267+ Y : cueast .NewList (e .makeType (typ .Elem (), kind )),
12521268 }
12531269 }
12541270
@@ -1259,7 +1275,7 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12591275
12601276 f := & cueast.Field {
12611277 Label : cueast .NewList (e .ident ("string" , false )),
1262- Value : e .makeType (typ .Elem ()),
1278+ Value : e .makeType (typ .Elem (), kind ),
12631279 }
12641280 cueast .SetRelPos (f , cuetoken .Blank )
12651281 return & cueast.StructLit {
@@ -1282,7 +1298,7 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12821298 case * types.Union :
12831299 var exprs []cueast.Expr
12841300 for term := range typ .Terms () {
1285- exprs = append (exprs , e .makeType (term .Type ()))
1301+ exprs = append (exprs , e .makeType (term .Type (), kind ))
12861302 }
12871303 return cueast .NewBinExpr (cuetoken .OR , exprs ... )
12881304
@@ -1305,12 +1321,12 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
13051321 //
13061322 var exprs []cueast.Expr
13071323 for etyp := range typ .EmbeddedTypes () {
1308- exprs = append (exprs , e .makeType (etyp ))
1324+ exprs = append (exprs , e .makeType (etyp , kind ))
13091325 }
13101326 return cueast .NewBinExpr (cuetoken .OR , exprs ... )
13111327
13121328 case * types.TypeParam :
1313- return e .makeType (typ .Constraint ())
1329+ return e .makeType (typ .Constraint (), kind )
13141330
13151331 default :
13161332 // record error
@@ -1360,7 +1376,7 @@ func (e *extractor) addFields(x *types.Struct, st *cueast.StructLit) {
13601376 }
13611377 switch typ := types .Unalias (typ ).(type ) {
13621378 case * types.Named :
1363- embed := & cueast.EmbedDecl {Expr : e .makeType (typ )}
1379+ embed := & cueast.EmbedDecl {Expr : e .makeType (typ , regular )}
13641380 if i > 0 {
13651381 cueast .SetRelPos (embed , cuetoken .NewSection )
13661382 }
@@ -1381,10 +1397,7 @@ func (e *extractor) addFields(x *types.Struct, st *cueast.StructLit) {
13811397 doc := docs [i ]
13821398
13831399 // TODO: check referrers
1384- kind := regular
1385- if e .isOptional (f , doc , tag ) {
1386- kind = optional
1387- }
1400+ kind := e .detectFieldKind (f , doc , tag )
13881401 field , cueType := e .makeField (name , kind , f .Type (), doc , count > 0 )
13891402 add (field )
13901403
@@ -1480,24 +1493,36 @@ func (e *extractor) isInline(tag string) bool {
14801493 hasFlag (tag , "yaml" , "inline" , 1 )
14811494}
14821495
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-
1496+ func (e * extractor ) detectFieldKind (f * types.Var , doc * ast.CommentGroup , tag string ) fieldKind {
1497+ // From k8s docs (https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md):
1498+ // - a map field marked with `+nullable` would accept either `foo: null` or `foo: {}`.
1499+ // - Using the `+optional` or the `omitempty` tag means that the field is optional.
1500+ // Note that for backward compatibility, any field that has the `omitempty` struct
1501+ // tag, and is not explicitly marked as `+required`, will be considered to be optional.
14881502 for line := range strings .SplitSeq (doc .Text (), "\n " ) {
14891503 before , _ , _ := strings .Cut (strings .TrimSpace (line ), "=" )
1490- if before == "+optional" {
1491- return true
1504+ switch before {
1505+ case "+nullable" :
1506+ return regular
1507+ case "+optional" :
1508+ return optional
14921509 }
14931510 }
14941511
1512+ if _ , ok := f .Type ().(* types.Pointer ); ok {
1513+ return optional
1514+ }
1515+
14951516 // Go 1.24 added the "omitzero" option to encoding/json, an improvement over "omitempty".
14961517 // Note that, as of mid 2025, YAML libraries don't seem to have picked up "omitzero" yet.
14971518 // TODO: also when the type is a list or other kind of pointer.
1498- return hasFlag (tag , "json" , "omitempty" , 1 ) ||
1519+ if hasFlag (tag , "json" , "omitempty" , 1 ) ||
14991520 hasFlag (tag , "json" , "omitzero" , 1 ) ||
1500- hasFlag (tag , "yaml" , "omitempty" , 1 )
1521+ hasFlag (tag , "yaml" , "omitempty" , 1 ) {
1522+ return optional
1523+ }
1524+
1525+ return regular
15011526}
15021527
15031528func hasFlag (tag , key , flag string , offset int ) bool {
0 commit comments