@@ -468,6 +468,295 @@ describe("v.object utility methods", () => {
468468 } ) ;
469469 } ) ;
470470
471+ describe ( "required" , ( ) => {
472+ test ( "makes all top-level fields required" , ( ) => {
473+ const original = v . object ( {
474+ a : v . optional ( v . string ( ) ) ,
475+ b : v . optional ( v . number ( ) ) ,
476+ c : v . optional ( v . boolean ( ) ) ,
477+ } ) ;
478+
479+ const required = original . required ( ) ;
480+
481+ // Type checks
482+ assert <
483+ Equals <
484+ Infer < typeof required > ,
485+ {
486+ a : string ;
487+ b : number ;
488+ c : boolean ;
489+ }
490+ >
491+ > ( ) ;
492+
493+ // Runtime checks
494+ expect ( required . fields . a . isOptional ) . toBe ( "required" ) ;
495+ expect ( required . fields . b . isOptional ) . toBe ( "required" ) ;
496+ expect ( required . fields . c . isOptional ) . toBe ( "required" ) ;
497+ expect ( required . isOptional ) . toBe ( "required" ) ;
498+ } ) ;
499+
500+ test ( "works with already required fields" , ( ) => {
501+ const original = v . object ( {
502+ a : v . string ( ) ,
503+ b : v . optional ( v . number ( ) ) ,
504+ c : v . boolean ( ) ,
505+ } ) ;
506+
507+ const required = original . required ( ) ;
508+
509+ // Type checks - all fields should be required
510+ type Result = Infer < typeof required > ;
511+ const _test1 : Result = { a : "hello" , b : 42 , c : true } ;
512+ // @ts -expect-error - fields should not be optional
513+ const _test2 : Result = { a : "hello" } ;
514+
515+ // Runtime checks
516+ expect ( required . fields . a . isOptional ) . toBe ( "required" ) ;
517+ expect ( required . fields . b . isOptional ) . toBe ( "required" ) ;
518+ expect ( required . fields . c . isOptional ) . toBe ( "required" ) ;
519+ } ) ;
520+
521+ test ( "does not recurse into nested objects" , ( ) => {
522+ const original = v . object ( {
523+ nested : v . optional ( v . object ( {
524+ inner : v . optional ( v . string ( ) ) ,
525+ required : v . number ( ) ,
526+ } ) ) ,
527+ simple : v . optional ( v . number ( ) ) ,
528+ } ) ;
529+
530+ const required = original . required ( ) ;
531+
532+ // Type checks - nested.inner remains optional
533+ type Result = Infer < typeof required > ;
534+ const _test : Result = {
535+ nested : { inner : "hello" , required : 42 } ,
536+ simple : 42 ,
537+ } ;
538+ const _test2 : Result = {
539+ // nested.inner is still optional, so this is valid
540+ nested : { required : 42 } ,
541+ simple : 42 ,
542+ } ;
543+
544+ // Runtime checks - top level
545+ expect ( required . fields . nested . isOptional ) . toBe ( "required" ) ;
546+ expect ( required . fields . simple . isOptional ) . toBe ( "required" ) ;
547+
548+ // Runtime checks - nested object fields remain unchanged (shallow)
549+ const nestedObj = required . fields . nested ;
550+ expect ( nestedObj . fields . inner . isOptional ) . toBe ( "optional" ) ;
551+ expect ( nestedObj . fields . required . isOptional ) . toBe ( "required" ) ;
552+ } ) ;
553+
554+ test ( "makes VObject itself required" , ( ) => {
555+ const original = v . object ( {
556+ a : v . optional ( v . string ( ) ) ,
557+ b : v . optional ( v . number ( ) ) ,
558+ } ) ;
559+ const optional = original . asOptional ( ) ;
560+ const required = optional . required ( ) ;
561+
562+ // Type checks
563+ type Result = Infer < typeof required > ;
564+ const _test : Result = { a : "hello" , b : 42 } ;
565+
566+ // Runtime check: Both VObject and fields become required
567+ expect ( required . isOptional ) . toBe ( "required" ) ;
568+ expect ( required . fields . a . isOptional ) . toBe ( "required" ) ;
569+ expect ( required . fields . b . isOptional ) . toBe ( "required" ) ;
570+ } ) ;
571+
572+ test ( "preserves validator properties" , ( ) => {
573+ const original = v . object ( {
574+ id : v . optional ( v . id ( "users" ) ) ,
575+ literal : v . optional ( v . literal ( "test" ) ) ,
576+ array : v . optional ( v . array ( v . string ( ) ) ) ,
577+ record : v . optional ( v . record ( v . string ( ) , v . number ( ) ) ) ,
578+ union : v . optional ( v . union ( v . string ( ) , v . number ( ) ) ) ,
579+ } ) ;
580+
581+ const required = original . required ( ) ;
582+
583+ // Check that specific validator properties are preserved
584+ expect ( ( required . fields . id ) . tableName ) . toBe ( "users" ) ;
585+ expect ( ( required . fields . literal ) . value ) . toBe ( "test" ) ;
586+ expect ( ( required . fields . array ) . element . kind ) . toBe ( "string" ) ;
587+ expect ( ( required . fields . record ) . key . kind ) . toBe ( "string" ) ;
588+ expect ( ( required . fields . record ) . value . kind ) . toBe ( "float64" ) ;
589+ expect ( ( required . fields . union ) . members ) . toHaveLength ( 2 ) ;
590+ } ) ;
591+ } ) ;
592+
593+ describe ( "deepRequired" , ( ) => {
594+ test ( "recursively makes all fields required including nested objects" , ( ) => {
595+ const original = v . object ( {
596+ nested : v . optional ( v . object ( {
597+ inner : v . optional ( v . string ( ) ) ,
598+ required : v . number ( ) ,
599+ } ) ) ,
600+ simple : v . optional ( v . number ( ) ) ,
601+ } ) ;
602+
603+ const required = original . deepRequired ( ) ;
604+
605+ // Type checks - nested.inner becomes required
606+ type Result = Infer < typeof required > ;
607+ const _test : Result = {
608+ nested : { inner : "hello" , required : 42 } ,
609+ simple : 42 ,
610+ } ;
611+ const _test2 : Result = {
612+ // @ts -expect-error - missing required property "inner"
613+ nested : { required : 42 } ,
614+ simple : 42 ,
615+ } ;
616+
617+ // Runtime checks - top level
618+ expect ( required . fields . nested . isOptional ) . toBe ( "required" ) ;
619+ expect ( required . fields . simple . isOptional ) . toBe ( "required" ) ;
620+
621+ // Runtime checks - nested object fields are also made required recursively
622+ const nestedObj = required . fields . nested ;
623+ expect ( nestedObj . fields . inner . isOptional ) . toBe ( "required" ) ;
624+ expect ( nestedObj . fields . required . isOptional ) . toBe ( "required" ) ;
625+ } ) ;
626+
627+ test ( "works with multiple levels of nesting" , ( ) => {
628+ const original = v . object ( {
629+ level1 : v . optional ( v . object ( {
630+ level2 : v . optional ( v . object ( {
631+ level3 : v . optional ( v . string ( ) ) ,
632+ } ) ) ,
633+ } ) ) ,
634+ } ) ;
635+
636+ const required = original . deepRequired ( ) ;
637+
638+ // Runtime checks - all levels become required
639+ const level1 = required . fields . level1 ;
640+ const level2 = level1 . fields . level2 ;
641+ expect ( level1 . isOptional ) . toBe ( "required" ) ;
642+ expect ( level2 . isOptional ) . toBe ( "required" ) ;
643+ expect ( level2 . fields . level3 . isOptional ) . toBe ( "required" ) ;
644+ } ) ;
645+
646+ test ( "recursion works with already-required nested objects" , ( ) => {
647+ const original = v . object ( {
648+ id : v . string ( ) ,
649+ profile : v . object ( {
650+ displayName : v . optional ( v . string ( ) ) ,
651+ isPublic : v . optional ( v . boolean ( ) )
652+ } ) ,
653+ tags : v . array ( v . string ( ) )
654+ } ) ;
655+
656+ const required = original . deepRequired ( ) ;
657+
658+ // Type checks - nested fields should be required
659+ type Result = Infer < typeof required > ;
660+ const _test : Result = {
661+ id : "123" ,
662+ profile : {
663+ displayName : "John" ,
664+ isPublic : true
665+ } ,
666+ tags : [ "tag1" ]
667+ } ;
668+
669+ const _testShouldError : Result = {
670+ id : "123" ,
671+ // @ts -expect-error - displayName should be required after recursion
672+ profile : {
673+ isPublic : true
674+ // missing displayName
675+ } ,
676+ tags : [ "tag1" ]
677+ } ;
678+
679+ // Runtime checks - verify recursion into already-required objects
680+ expect ( required . fields . profile . isOptional ) . toBe ( "required" ) ;
681+ const profileObj = required . fields . profile ;
682+ expect ( profileObj . fields . displayName . isOptional ) . toBe ( "required" ) ;
683+ expect ( profileObj . fields . isPublic . isOptional ) . toBe ( "required" ) ;
684+ } ) ;
685+ } ) ;
686+
687+ describe ( "required vs deepRequired" , ( ) => {
688+ test ( "required is shallow, deepRequired is deep" , ( ) => {
689+ const original = v . object ( {
690+ a : v . optional ( v . string ( ) ) ,
691+ nested : v . optional ( v . object ( {
692+ inner : v . optional ( v . string ( ) ) ,
693+ } ) ) ,
694+ } ) ;
695+
696+ const shallow = original . required ( ) ;
697+ const deep = original . deepRequired ( ) ;
698+
699+ // Both make top-level required
700+ expect ( shallow . fields . a . isOptional ) . toBe ( "required" ) ;
701+ expect ( deep . fields . a . isOptional ) . toBe ( "required" ) ;
702+ expect ( shallow . fields . nested . isOptional ) . toBe ( "required" ) ;
703+ expect ( deep . fields . nested . isOptional ) . toBe ( "required" ) ;
704+
705+ // Shallow: nested fields stay optional
706+ const shallowNested = shallow . fields . nested ;
707+ expect ( shallowNested . fields . inner . isOptional ) . toBe ( "optional" ) ;
708+
709+ // Deep: nested fields become required
710+ const deepNested = deep . fields . nested ;
711+ expect ( deepNested . fields . inner . isOptional ) . toBe ( "required" ) ;
712+ } ) ;
713+ } ) ;
714+
715+ describe ( "asOptional vs partial" , ( ) => {
716+ test ( "asOptional only affects object, partial affects fields" , ( ) => {
717+ const original = v . object ( {
718+ a : v . string ( ) ,
719+ b : v . optional ( v . number ( ) ) ,
720+ } ) ;
721+
722+ const asOptional = original . asOptional ( ) ;
723+ const partial = original . partial ( ) ;
724+
725+ // asOptional: only object becomes optional, fields unchanged
726+ expect ( asOptional . isOptional ) . toBe ( "optional" ) ;
727+ expect ( asOptional . fields . a . isOptional ) . toBe ( "required" ) ;
728+ expect ( asOptional . fields . b . isOptional ) . toBe ( "optional" ) ;
729+
730+ // partial: object unchanged, all fields become optional
731+ expect ( partial . isOptional ) . toBe ( "required" ) ;
732+ expect ( partial . fields . a . isOptional ) . toBe ( "optional" ) ;
733+ expect ( partial . fields . b . isOptional ) . toBe ( "optional" ) ;
734+ } ) ;
735+ } ) ;
736+
737+ describe ( "asRequired vs required" , ( ) => {
738+ test ( "asRequired only affects object, required affects fields" , ( ) => {
739+ const original = v . object ( {
740+ a : v . string ( ) ,
741+ b : v . optional ( v . number ( ) ) ,
742+ } ) ;
743+ const optional = original . asOptional ( ) ;
744+
745+ const asRequired = optional . asRequired ( ) ;
746+ const required = optional . required ( ) ;
747+
748+ // asRequired: only object becomes required, fields unchanged
749+ expect ( asRequired . isOptional ) . toBe ( "required" ) ;
750+ expect ( asRequired . fields . a . isOptional ) . toBe ( "required" ) ;
751+ expect ( asRequired . fields . b . isOptional ) . toBe ( "optional" ) ;
752+
753+ // required: both object and top-level fields become required
754+ expect ( required . isOptional ) . toBe ( "required" ) ;
755+ expect ( required . fields . a . isOptional ) . toBe ( "required" ) ;
756+ expect ( required . fields . b . isOptional ) . toBe ( "required" ) ;
757+ } ) ;
758+ } ) ;
759+
471760 describe ( "chaining utility methods" , ( ) => {
472761 test ( "can chain multiple operations" , ( ) => {
473762 const base = v . object ( {
@@ -499,6 +788,79 @@ describe("v.object utility methods", () => {
499788 expect ( result . fields . a . isOptional ) . toBe ( "optional" ) ;
500789 } ) ;
501790
791+ test ( "can chain operations including required()" , ( ) => {
792+ const base = v . object ( {
793+ a : v . optional ( v . string ( ) ) ,
794+ b : v . optional ( v . number ( ) ) ,
795+ c : v . optional ( v . boolean ( ) ) ,
796+ d : v . optional ( v . int64 ( ) ) ,
797+ } ) ;
798+
799+ const result = base . required ( ) . omit ( "d" ) . extend ( { e : v . optional ( v . bytes ( ) ) } ) ;
800+
801+ // Type checks
802+ type Result = Infer < typeof result > ;
803+ const _test1 : Result = {
804+ a : "hello" ,
805+ b : 42 ,
806+ c : true ,
807+ e : new ArrayBuffer ( 0 ) ,
808+ } ;
809+ const _test2 : Result = {
810+ a : "hello" ,
811+ b : 42 ,
812+ c : true ,
813+ // e is optional
814+ } ;
815+
816+ // Runtime checks
817+ expect ( result . fields ) . toHaveProperty ( "a" ) ;
818+ expect ( result . fields ) . toHaveProperty ( "b" ) ;
819+ expect ( result . fields ) . toHaveProperty ( "c" ) ;
820+ expect ( result . fields ) . toHaveProperty ( "e" ) ;
821+ expect ( result . fields ) . not . toHaveProperty ( "d" ) ;
822+
823+ // Original fields became required, new field is optional
824+ expect ( result . fields . a . isOptional ) . toBe ( "required" ) ;
825+ expect ( result . fields . b . isOptional ) . toBe ( "required" ) ;
826+ expect ( result . fields . c . isOptional ) . toBe ( "required" ) ;
827+ expect ( result . fields . e . isOptional ) . toBe ( "optional" ) ;
828+ } ) ;
829+
830+ test ( "required() in complex chain" , ( ) => {
831+ const base = v . object ( {
832+ keep : v . string ( ) ,
833+ remove : v . number ( ) ,
834+ makeOptional : v . boolean ( ) ,
835+ } ) ;
836+
837+ // partial -> pick -> extend -> required
838+ const result = base
839+ . partial ( )
840+ . pick ( "keep" , "makeOptional" )
841+ . extend ( {
842+ newRequired : v . string ( ) ,
843+ newOptional : v . optional ( v . number ( ) ) ,
844+ } )
845+ . required ( ) ;
846+
847+ // Type checks
848+ type Result = Infer < typeof result > ;
849+ const _test : Result = {
850+ keep : "hello" ,
851+ makeOptional : true ,
852+ newRequired : "world" ,
853+ newOptional : 42 ,
854+ } ;
855+
856+ // Runtime checks
857+ expect ( result . fields . keep . isOptional ) . toBe ( "required" ) ;
858+ expect ( result . fields . makeOptional . isOptional ) . toBe ( "required" ) ;
859+ expect ( result . fields . newRequired . isOptional ) . toBe ( "required" ) ;
860+ expect ( result . fields . newOptional . isOptional ) . toBe ( "required" ) ;
861+ expect ( result . fields ) . not . toHaveProperty ( "remove" ) ;
862+ } ) ;
863+
502864 test ( "complex chaining scenario" , ( ) => {
503865 const user = v . object ( {
504866 name : v . string ( ) ,
0 commit comments