@@ -691,6 +691,328 @@ describe("convertJsonSchemaToZod", () => {
691691 ] ;
692692 expect ( ( ) => zodSchema . parse ( duplicateObjects ) ) . toThrow ( ) ;
693693 } ) ;
694+
695+ describe ( "Tuple arrays (items as array)" , ( ) => {
696+ it ( "should handle tuple array with different types" , ( ) => {
697+ const jsonSchema = {
698+ $schema : "https://json-schema.org/draft/2020-12/schema" ,
699+ type : "array" ,
700+ items : [
701+ { type : "string" } ,
702+ { type : "number" } ,
703+ { type : "boolean" }
704+ ]
705+ } ;
706+
707+ const zodSchema = convertJsonSchemaToZod ( jsonSchema ) ;
708+
709+ // Valid tuple should pass
710+ expect ( ( ) => zodSchema . parse ( [ "hello" , 42 , true ] ) ) . not . toThrow ( ) ;
711+
712+ // Wrong types should fail
713+ expect ( ( ) => zodSchema . parse ( [ 42 , "hello" , true ] ) ) . toThrow ( ) ;
714+ expect ( ( ) => zodSchema . parse ( [ "hello" , "world" , true ] ) ) . toThrow ( ) ;
715+
716+ // Wrong length should fail
717+ expect ( ( ) => zodSchema . parse ( [ "hello" , 42 ] ) ) . toThrow ( ) ;
718+ expect ( ( ) => zodSchema . parse ( [ "hello" , 42 , true , "extra" ] ) ) . toThrow ( ) ;
719+ } ) ;
720+
721+ it ( "should handle tuple array with single item type" , ( ) => {
722+ const jsonSchema = {
723+ $schema : "https://json-schema.org/draft/2020-12/schema" ,
724+ type : "array" ,
725+ items : [
726+ { type : "string" }
727+ ]
728+ } ;
729+
730+ const zodSchema = convertJsonSchemaToZod ( jsonSchema ) ;
731+
732+ expect ( ( ) => zodSchema . parse ( [ "hello" ] ) ) . not . toThrow ( ) ;
733+ expect ( ( ) => zodSchema . parse ( [ 42 ] ) ) . toThrow ( ) ;
734+ expect ( ( ) => zodSchema . parse ( [ "hello" , "world" ] ) ) . toThrow ( ) ;
735+ expect ( ( ) => zodSchema . parse ( [ ] ) ) . toThrow ( ) ;
736+ } ) ;
737+
738+ it ( "should handle empty tuple array" , ( ) => {
739+ const jsonSchema = {
740+ $schema : "https://json-schema.org/draft/2020-12/schema" ,
741+ type : "array" ,
742+ items : [ ]
743+ } ;
744+
745+ const zodSchema = convertJsonSchemaToZod ( jsonSchema ) ;
746+
747+ expect ( ( ) => zodSchema . parse ( [ ] ) ) . not . toThrow ( ) ;
748+ expect ( ( ) => zodSchema . parse ( [ "anything" ] ) ) . toThrow ( ) ;
749+ } ) ;
750+
751+ it ( "should handle tuple array with complex item types" , ( ) => {
752+ const jsonSchema = {
753+ $schema : "https://json-schema.org/draft/2020-12/schema" ,
754+ type : "array" ,
755+ items : [
756+ {
757+ type : "object" ,
758+ properties : {
759+ name : { type : "string" }
760+ } ,
761+ required : [ "name" ]
762+ } ,
763+ { type : "number" , minimum : 0 } ,
764+ {
765+ type : "array" ,
766+ items : { type : "string" }
767+ }
768+ ]
769+ } ;
770+
771+ const zodSchema = convertJsonSchemaToZod ( jsonSchema ) ;
772+
773+ // Valid tuple should pass
774+ expect ( ( ) => zodSchema . parse ( [
775+ { name : "John" } ,
776+ 5 ,
777+ [ "a" , "b" , "c" ]
778+ ] ) ) . not . toThrow ( ) ;
779+
780+ // Invalid object should fail
781+ expect ( ( ) => zodSchema . parse ( [
782+ { age : 25 } ,
783+ 5 ,
784+ [ "a" , "b" , "c" ]
785+ ] ) ) . toThrow ( ) ;
786+
787+ // Invalid number should fail
788+ expect ( ( ) => zodSchema . parse ( [
789+ { name : "John" } ,
790+ - 5 ,
791+ [ "a" , "b" , "c" ]
792+ ] ) ) . toThrow ( ) ;
793+
794+ // Invalid nested array should fail
795+ expect ( ( ) => zodSchema . parse ( [
796+ { name : "John" } ,
797+ 5 ,
798+ [ "a" , 123 , "c" ]
799+ ] ) ) . toThrow ( ) ;
800+ } ) ;
801+
802+ it ( "should convert tuple to proper JSON schema" , ( ) => {
803+ const jsonSchema = {
804+ $schema : "https://json-schema.org/draft/2020-12/schema" ,
805+ type : "array" ,
806+ items : [
807+ { type : "string" } ,
808+ { type : "number" }
809+ ]
810+ } ;
811+
812+ const zodSchema = convertJsonSchemaToZod ( jsonSchema ) ;
813+ const resultSchema = z . toJSONSchema ( zodSchema ) ;
814+
815+ // Zod converts tuples to use prefixItems (which is correct for draft 2020-12)
816+ expect ( resultSchema . type ) . toEqual ( "array" ) ;
817+ expect ( resultSchema . prefixItems ) . toEqual ( [
818+ { type : "string" } ,
819+ { type : "number" }
820+ ] ) ;
821+ } ) ;
822+ } ) ;
823+
824+ describe ( "prefixItems (Draft 2020-12 tuples)" , ( ) => {
825+ it ( "should handle prefixItems with different types" , ( ) => {
826+ const jsonSchema = {
827+ $schema : "https://json-schema.org/draft/2020-12/schema" ,
828+ type : "array" ,
829+ prefixItems : [
830+ { type : "string" } ,
831+ { type : "number" } ,
832+ { type : "boolean" }
833+ ]
834+ } ;
835+
836+ const zodSchema = convertJsonSchemaToZod ( jsonSchema ) ;
837+
838+ // Valid tuple should pass
839+ expect ( ( ) => zodSchema . parse ( [ "hello" , 42 , true ] ) ) . not . toThrow ( ) ;
840+
841+ // Wrong types should fail
842+ expect ( ( ) => zodSchema . parse ( [ 42 , "hello" , true ] ) ) . toThrow ( ) ;
843+ expect ( ( ) => zodSchema . parse ( [ "hello" , "world" , true ] ) ) . toThrow ( ) ;
844+
845+ // Partial tuples should be allowed - prefixItems doesn't require all items
846+ expect ( ( ) => zodSchema . parse ( [ "hello" ] ) ) . not . toThrow ( ) ;
847+ expect ( ( ) => zodSchema . parse ( [ "hello" , 42 ] ) ) . not . toThrow ( ) ;
848+
849+ // Additional items should be allowed by default with prefixItems
850+ expect ( ( ) => zodSchema . parse ( [ "hello" , 42 , true , "extra" ] ) ) . not . toThrow ( ) ;
851+ expect ( ( ) => zodSchema . parse ( [ "hello" , 42 , true , 999 , { any : "thing" } ] ) ) . not . toThrow ( ) ;
852+ } ) ;
853+
854+ it ( "should handle prefixItems with single item type" , ( ) => {
855+ const jsonSchema = {
856+ $schema : "https://json-schema.org/draft/2020-12/schema" ,
857+ type : "array" ,
858+ prefixItems : [
859+ { type : "string" }
860+ ]
861+ } ;
862+
863+ const zodSchema = convertJsonSchemaToZod ( jsonSchema ) ;
864+
865+ expect ( ( ) => zodSchema . parse ( [ "hello" ] ) ) . not . toThrow ( ) ;
866+ expect ( ( ) => zodSchema . parse ( [ 42 ] ) ) . toThrow ( ) ;
867+ expect ( ( ) => zodSchema . parse ( [ "hello" , "world" ] ) ) . not . toThrow ( ) ; // extra items allowed
868+ expect ( ( ) => zodSchema . parse ( [ ] ) ) . not . toThrow ( ) ; // empty array is valid - no items required
869+ } ) ;
870+
871+ it ( "should handle empty prefixItems array" , ( ) => {
872+ const jsonSchema = {
873+ $schema : "https://json-schema.org/draft/2020-12/schema" ,
874+ type : "array" ,
875+ prefixItems : [ ]
876+ } ;
877+
878+ const zodSchema = convertJsonSchemaToZod ( jsonSchema ) ;
879+
880+ expect ( ( ) => zodSchema . parse ( [ ] ) ) . not . toThrow ( ) ;
881+ expect ( ( ) => zodSchema . parse ( [ "anything" ] ) ) . not . toThrow ( ) ; // extra items allowed with empty prefixItems
882+ expect ( ( ) => zodSchema . parse ( [ 1 , 2 , 3 ] ) ) . not . toThrow ( ) ;
883+ } ) ;
884+
885+ it ( "should handle prefixItems with complex nested types" , ( ) => {
886+ const jsonSchema = {
887+ $schema : "https://json-schema.org/draft/2020-12/schema" ,
888+ type : "array" ,
889+ prefixItems : [
890+ {
891+ type : "object" ,
892+ properties : {
893+ id : { type : "number" } ,
894+ name : { type : "string" }
895+ } ,
896+ required : [ "id" , "name" ]
897+ } ,
898+ {
899+ type : "array" ,
900+ items : { type : "string" }
901+ } ,
902+ { type : "number" , minimum : 0 , maximum : 100 }
903+ ]
904+ } ;
905+
906+ const zodSchema = convertJsonSchemaToZod ( jsonSchema ) ;
907+
908+ // Valid tuple should pass
909+ expect ( ( ) => zodSchema . parse ( [
910+ { id : 1 , name : "Alice" } ,
911+ [ "tag1" , "tag2" ] ,
912+ 50
913+ ] ) ) . not . toThrow ( ) ;
914+
915+ // Invalid object should fail
916+ expect ( ( ) => zodSchema . parse ( [
917+ { name : "Alice" } , // missing id
918+ [ "tag1" , "tag2" ] ,
919+ 50
920+ ] ) ) . toThrow ( ) ;
921+
922+ // Invalid array should fail
923+ expect ( ( ) => zodSchema . parse ( [
924+ { id : 1 , name : "Alice" } ,
925+ [ "tag1" , 123 ] , // number in string array
926+ 50
927+ ] ) ) . toThrow ( ) ;
928+
929+ // Invalid number should fail
930+ expect ( ( ) => zodSchema . parse ( [
931+ { id : 1 , name : "Alice" } ,
932+ [ "tag1" , "tag2" ] ,
933+ 150 // exceeds maximum
934+ ] ) ) . toThrow ( ) ;
935+ } ) ;
936+
937+ it ( "should validate prefixItems behavior correctly" , ( ) => {
938+ const jsonSchema = {
939+ $schema : "https://json-schema.org/draft/2020-12/schema" ,
940+ type : "array" ,
941+ prefixItems : [
942+ { type : "string" } ,
943+ { type : "number" }
944+ ]
945+ } ;
946+
947+ const zodSchema = convertJsonSchemaToZod ( jsonSchema ) ;
948+
949+ // Test the behavior instead of schema round-trip since we use custom validation
950+ expect ( ( ) => zodSchema . parse ( [ "hello" , 42 ] ) ) . not . toThrow ( ) ;
951+ expect ( ( ) => zodSchema . parse ( [ "hello" ] ) ) . not . toThrow ( ) ;
952+ expect ( ( ) => zodSchema . parse ( [ ] ) ) . not . toThrow ( ) ;
953+ expect ( ( ) => zodSchema . parse ( [ "hello" , 42 , "extra" ] ) ) . not . toThrow ( ) ;
954+ expect ( ( ) => zodSchema . parse ( [ 42 , "hello" ] ) ) . toThrow ( ) ;
955+ } ) ;
956+
957+ it ( "should handle prefixItems with constraints" , ( ) => {
958+ const jsonSchema = {
959+ $schema : "https://json-schema.org/draft/2020-12/schema" ,
960+ type : "array" ,
961+ prefixItems : [
962+ { type : "string" , minLength : 2 } ,
963+ { type : "number" , minimum : 0 }
964+ ] ,
965+ minItems : 2 ,
966+ maxItems : 2
967+ } ;
968+
969+ const zodSchema = convertJsonSchemaToZod ( jsonSchema ) ;
970+
971+ expect ( ( ) => zodSchema . parse ( [ "hi" , 5 ] ) ) . not . toThrow ( ) ;
972+ expect ( ( ) => zodSchema . parse ( [ "a" , 5 ] ) ) . toThrow ( ) ; // string too short
973+ expect ( ( ) => zodSchema . parse ( [ "hi" , - 1 ] ) ) . toThrow ( ) ; // number too small
974+ } ) ;
975+
976+ it ( "should handle prefixItems with items: false (strict tuple)" , ( ) => {
977+ const jsonSchema = {
978+ $schema : "https://json-schema.org/draft/2020-12/schema" ,
979+ type : "array" ,
980+ prefixItems : [
981+ { type : "string" } ,
982+ { type : "number" }
983+ ] ,
984+ items : false
985+ } ;
986+
987+ const zodSchema = convertJsonSchemaToZod ( jsonSchema ) ;
988+
989+ expect ( ( ) => zodSchema . parse ( [ "hello" , 42 ] ) ) . not . toThrow ( ) ;
990+ expect ( ( ) => zodSchema . parse ( [ "hello" , 42 , "extra" ] ) ) . toThrow ( ) ; // no additional items allowed
991+ expect ( ( ) => zodSchema . parse ( [ "hello" ] ) ) . not . toThrow ( ) ; // partial tuple OK
992+ expect ( ( ) => zodSchema . parse ( [ ] ) ) . not . toThrow ( ) ; // empty array OK
993+ } ) ;
994+
995+ it ( "should handle prefixItems with items schema (constrained additional items)" , ( ) => {
996+ const jsonSchema = {
997+ $schema : "https://json-schema.org/draft/2020-12/schema" ,
998+ type : "array" ,
999+ prefixItems : [
1000+ { type : "string" } ,
1001+ { type : "number" }
1002+ ] ,
1003+ items : { type : "boolean" }
1004+ } ;
1005+
1006+ const zodSchema = convertJsonSchemaToZod ( jsonSchema ) ;
1007+
1008+ expect ( ( ) => zodSchema . parse ( [ "hello" , 42 ] ) ) . not . toThrow ( ) ;
1009+ expect ( ( ) => zodSchema . parse ( [ "hello" , 42 , true ] ) ) . not . toThrow ( ) ;
1010+ expect ( ( ) => zodSchema . parse ( [ "hello" , 42 , true , false ] ) ) . not . toThrow ( ) ;
1011+ expect ( ( ) => zodSchema . parse ( [ "hello" , 42 , "string" ] ) ) . toThrow ( ) ; // additional item wrong type
1012+ expect ( ( ) => zodSchema . parse ( [ "hello" ] ) ) . not . toThrow ( ) ; // partial tuple OK
1013+ expect ( ( ) => zodSchema . parse ( [ ] ) ) . not . toThrow ( ) ; // empty array OK
1014+ } ) ;
1015+ } ) ;
6941016 } ) ;
6951017} ) ;
6961018
0 commit comments