@alt-stack/zod-openapi 1.1.1 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
 
2
- > @alt-stack/zod-openapi@1.1.1 build /home/runner/work/alt-stack/alt-stack/packages/zod-openapi
2
+ > @alt-stack/zod-openapi@1.1.3 build /home/runner/work/alt-stack/alt-stack/packages/zod-openapi
3
3
  > tsup
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -10,13 +10,13 @@
10
10
  CLI Cleaning output folder
11
11
  ESM Build start
12
12
  CJS Build start
13
- CJS dist/index.cjs 26.95 KB
14
- CJS dist/index.cjs.map 55.60 KB
15
- CJS ⚡️ Build success in 37ms
16
- ESM dist/index.js 25.41 KB
17
- ESM dist/index.js.map 54.74 KB
18
- ESM ⚡️ Build success in 37ms
13
+ ESM dist/index.js 32.90 KB
14
+ ESM dist/index.js.map 72.42 KB
15
+ ESM ⚡️ Build success in 47ms
16
+ CJS dist/index.cjs 34.44 KB
17
+ CJS dist/index.cjs.map 73.28 KB
18
+ CJS ⚡️ Build success in 47ms
19
19
  DTS Build start
20
- DTS ⚡️ Build success in 1339ms
20
+ DTS ⚡️ Build success in 1504ms
21
21
  DTS dist/index.d.ts 4.83 KB
22
22
  DTS dist/index.d.cts 4.83 KB
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Anthony Altieri
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.cjs CHANGED
@@ -142,6 +142,87 @@ function topologicalSortSchemas(schemas) {
142
142
  return sorted;
143
143
  }
144
144
 
145
+ // src/schema-dedup.ts
146
+ function sortObjectDeep(obj) {
147
+ if (obj === null || typeof obj !== "object") return obj;
148
+ if (Array.isArray(obj)) return obj.map(sortObjectDeep);
149
+ const sorted = {};
150
+ const keys = Object.keys(obj).sort();
151
+ for (const key of keys) {
152
+ sorted[key] = sortObjectDeep(obj[key]);
153
+ }
154
+ return sorted;
155
+ }
156
+ function getSchemaFingerprint(schema) {
157
+ return JSON.stringify(sortObjectDeep(schema));
158
+ }
159
+ function createSchemaRegistry() {
160
+ return {
161
+ fingerprintToName: /* @__PURE__ */ new Map(),
162
+ nameToFingerprint: /* @__PURE__ */ new Map()
163
+ };
164
+ }
165
+ function registerSchema(registry, name, schema) {
166
+ const fingerprint = getSchemaFingerprint(schema);
167
+ const existing = registry.fingerprintToName.get(fingerprint);
168
+ if (existing) {
169
+ return { isNew: false, canonicalName: existing };
170
+ }
171
+ registry.fingerprintToName.set(fingerprint, name);
172
+ registry.nameToFingerprint.set(name, fingerprint);
173
+ return { isNew: true, canonicalName: name };
174
+ }
175
+ function preRegisterSchema(registry, name, fingerprint) {
176
+ registry.fingerprintToName.set(fingerprint, name);
177
+ registry.nameToFingerprint.set(name, fingerprint);
178
+ }
179
+ function extractErrorCode(schema) {
180
+ const properties = schema?.["properties"];
181
+ const errorObj = properties?.["error"];
182
+ const errorProps = errorObj?.["properties"];
183
+ const codeSchema = errorProps?.["code"];
184
+ const codeEnum = codeSchema?.["enum"];
185
+ if (Array.isArray(codeEnum) && codeEnum.length === 1) {
186
+ return codeEnum[0];
187
+ }
188
+ return null;
189
+ }
190
+ function errorCodeToPascalCase(code) {
191
+ return code.split("_").map((part) => part.charAt(0) + part.slice(1).toLowerCase()).join("");
192
+ }
193
+ function generateCommonErrorSchemaName(errorCode) {
194
+ return `${errorCodeToPascalCase(errorCode)}ErrorSchema`;
195
+ }
196
+ function findCommonSchemas(schemas, minCount = 2) {
197
+ const fingerprints = /* @__PURE__ */ new Map();
198
+ for (const { name, schema } of schemas) {
199
+ const fingerprint = getSchemaFingerprint(schema);
200
+ const existing = fingerprints.get(fingerprint);
201
+ if (existing) {
202
+ existing.names.push(name);
203
+ } else {
204
+ fingerprints.set(fingerprint, {
205
+ schema,
206
+ names: [name],
207
+ errorCode: extractErrorCode(schema)
208
+ });
209
+ }
210
+ }
211
+ const commonSchemas = [];
212
+ for (const [fingerprint, data] of fingerprints) {
213
+ if (data.names.length >= minCount) {
214
+ const name = data.errorCode ? generateCommonErrorSchemaName(data.errorCode) : data.names[0];
215
+ commonSchemas.push({
216
+ name,
217
+ schema: data.schema,
218
+ fingerprint,
219
+ count: data.names.length
220
+ });
221
+ }
222
+ }
223
+ return commonSchemas.sort((a, b) => b.count - a.count);
224
+ }
225
+
145
226
  // src/types/boolean.ts
146
227
  function convertOpenAPIBooleanToZod(_) {
147
228
  return "z.boolean()";
@@ -640,34 +721,64 @@ function generateRouteSchemaName2(path, method, suffix) {
640
721
  const parts = [methodPrefix, ...pathParts, suffix];
641
722
  return parts.join("");
642
723
  }
643
- function generateRouteSchemas(routes, convertSchema) {
644
- const lines = [];
645
- const schemaNames = /* @__PURE__ */ new Set();
724
+ function generateRouteSchemas(routes, convertSchema, registry) {
725
+ const declarations = [];
726
+ const schemaNameToCanonical = /* @__PURE__ */ new Map();
727
+ const generatedNames = /* @__PURE__ */ new Set();
646
728
  for (const route of routes) {
647
729
  const names = generateRouteSchemaNames(route);
648
730
  const pathParams = route.parameters.filter((p) => p.in === "path");
649
731
  const queryParams = route.parameters.filter((p) => p.in === "query");
650
732
  const headerParams = route.parameters.filter((p) => p.in === "header");
651
733
  if (names.paramsSchemaName && pathParams.length > 0) {
652
- if (!schemaNames.has(names.paramsSchemaName)) {
653
- schemaNames.add(names.paramsSchemaName);
734
+ const paramsSchema = {
735
+ type: "object",
736
+ properties: Object.fromEntries(
737
+ pathParams.map((p) => [p.name, p.schema])
738
+ ),
739
+ required: pathParams.filter((p) => p.required).map((p) => p.name)
740
+ };
741
+ const { isNew, canonicalName } = registerSchema(
742
+ registry,
743
+ names.paramsSchemaName,
744
+ paramsSchema
745
+ );
746
+ schemaNameToCanonical.set(names.paramsSchemaName, canonicalName);
747
+ if (isNew && !generatedNames.has(names.paramsSchemaName)) {
748
+ generatedNames.add(names.paramsSchemaName);
654
749
  const properties = [];
655
- const required = [];
656
750
  for (const param of pathParams) {
657
751
  const zodExpr = convertSchema(param.schema);
658
752
  properties.push(`${quotePropertyName2(param.name)}: ${zodExpr}`);
659
- if (param.required) {
660
- required.push(param.name);
661
- }
662
753
  }
663
- lines.push(
754
+ declarations.push(
664
755
  `export const ${names.paramsSchemaName} = z.object({ ${properties.join(", ")} });`
665
756
  );
757
+ } else if (!isNew && names.paramsSchemaName !== canonicalName) {
758
+ if (!generatedNames.has(names.paramsSchemaName)) {
759
+ generatedNames.add(names.paramsSchemaName);
760
+ declarations.push(
761
+ `export const ${names.paramsSchemaName} = ${canonicalName};`
762
+ );
763
+ }
666
764
  }
667
765
  }
668
766
  if (names.querySchemaName && queryParams.length > 0) {
669
- if (!schemaNames.has(names.querySchemaName)) {
670
- schemaNames.add(names.querySchemaName);
767
+ const querySchema = {
768
+ type: "object",
769
+ properties: Object.fromEntries(
770
+ queryParams.map((p) => [p.name, p.schema])
771
+ ),
772
+ required: queryParams.filter((p) => p.required).map((p) => p.name)
773
+ };
774
+ const { isNew, canonicalName } = registerSchema(
775
+ registry,
776
+ names.querySchemaName,
777
+ querySchema
778
+ );
779
+ schemaNameToCanonical.set(names.querySchemaName, canonicalName);
780
+ if (isNew && !generatedNames.has(names.querySchemaName)) {
781
+ generatedNames.add(names.querySchemaName);
671
782
  const properties = [];
672
783
  for (const param of queryParams) {
673
784
  let zodExpr = convertSchema(param.schema);
@@ -676,14 +787,34 @@ function generateRouteSchemas(routes, convertSchema) {
676
787
  }
677
788
  properties.push(`${quotePropertyName2(param.name)}: ${zodExpr}`);
678
789
  }
679
- lines.push(
790
+ declarations.push(
680
791
  `export const ${names.querySchemaName} = z.object({ ${properties.join(", ")} });`
681
792
  );
793
+ } else if (!isNew && names.querySchemaName !== canonicalName) {
794
+ if (!generatedNames.has(names.querySchemaName)) {
795
+ generatedNames.add(names.querySchemaName);
796
+ declarations.push(
797
+ `export const ${names.querySchemaName} = ${canonicalName};`
798
+ );
799
+ }
682
800
  }
683
801
  }
684
802
  if (names.headersSchemaName && headerParams.length > 0) {
685
- if (!schemaNames.has(names.headersSchemaName)) {
686
- schemaNames.add(names.headersSchemaName);
803
+ const headersSchema = {
804
+ type: "object",
805
+ properties: Object.fromEntries(
806
+ headerParams.map((p) => [p.name, p.schema])
807
+ ),
808
+ required: headerParams.filter((p) => p.required).map((p) => p.name)
809
+ };
810
+ const { isNew, canonicalName } = registerSchema(
811
+ registry,
812
+ names.headersSchemaName,
813
+ headersSchema
814
+ );
815
+ schemaNameToCanonical.set(names.headersSchemaName, canonicalName);
816
+ if (isNew && !generatedNames.has(names.headersSchemaName)) {
817
+ generatedNames.add(names.headersSchemaName);
687
818
  const properties = [];
688
819
  for (const param of headerParams) {
689
820
  let zodExpr = convertSchema(param.schema);
@@ -692,16 +823,36 @@ function generateRouteSchemas(routes, convertSchema) {
692
823
  }
693
824
  properties.push(`${quotePropertyName2(param.name)}: ${zodExpr}`);
694
825
  }
695
- lines.push(
826
+ declarations.push(
696
827
  `export const ${names.headersSchemaName} = z.object({ ${properties.join(", ")} });`
697
828
  );
829
+ } else if (!isNew && names.headersSchemaName !== canonicalName) {
830
+ if (!generatedNames.has(names.headersSchemaName)) {
831
+ generatedNames.add(names.headersSchemaName);
832
+ declarations.push(
833
+ `export const ${names.headersSchemaName} = ${canonicalName};`
834
+ );
835
+ }
698
836
  }
699
837
  }
700
838
  if (names.bodySchemaName && route.requestBody) {
701
- if (!schemaNames.has(names.bodySchemaName)) {
702
- schemaNames.add(names.bodySchemaName);
839
+ const { isNew, canonicalName } = registerSchema(
840
+ registry,
841
+ names.bodySchemaName,
842
+ route.requestBody
843
+ );
844
+ schemaNameToCanonical.set(names.bodySchemaName, canonicalName);
845
+ if (isNew && !generatedNames.has(names.bodySchemaName)) {
846
+ generatedNames.add(names.bodySchemaName);
703
847
  const zodExpr = convertSchema(route.requestBody);
704
- lines.push(`export const ${names.bodySchemaName} = ${zodExpr};`);
848
+ declarations.push(`export const ${names.bodySchemaName} = ${zodExpr};`);
849
+ } else if (!isNew && names.bodySchemaName !== canonicalName) {
850
+ if (!generatedNames.has(names.bodySchemaName)) {
851
+ generatedNames.add(names.bodySchemaName);
852
+ declarations.push(
853
+ `export const ${names.bodySchemaName} = ${canonicalName};`
854
+ );
855
+ }
705
856
  }
706
857
  }
707
858
  for (const [statusCode, responseSchema] of Object.entries(
@@ -715,19 +866,35 @@ function generateRouteSchemas(routes, convertSchema) {
715
866
  route.method,
716
867
  suffix
717
868
  );
718
- if (!schemaNames.has(responseSchemaName)) {
719
- schemaNames.add(responseSchemaName);
869
+ const { isNew, canonicalName } = registerSchema(
870
+ registry,
871
+ responseSchemaName,
872
+ responseSchema
873
+ );
874
+ schemaNameToCanonical.set(responseSchemaName, canonicalName);
875
+ if (isNew && !generatedNames.has(responseSchemaName)) {
876
+ generatedNames.add(responseSchemaName);
720
877
  const zodExpr = convertSchema(responseSchema);
721
- lines.push(`export const ${responseSchemaName} = ${zodExpr};`);
878
+ declarations.push(`export const ${responseSchemaName} = ${zodExpr};`);
879
+ } else if (!isNew && responseSchemaName !== canonicalName) {
880
+ if (!generatedNames.has(responseSchemaName)) {
881
+ generatedNames.add(responseSchemaName);
882
+ declarations.push(
883
+ `export const ${responseSchemaName} = ${canonicalName};`
884
+ );
885
+ }
722
886
  }
723
887
  }
724
888
  }
725
- return lines;
889
+ return { declarations, schemaNameToCanonical };
726
890
  }
727
- function generateRequestResponseObjects(routes) {
891
+ function generateRequestResponseObjects(routes, schemaNameToCanonical) {
728
892
  const lines = [];
729
893
  const requestPaths = {};
730
894
  const responsePaths = {};
895
+ const resolveSchemaName = (name) => {
896
+ return schemaNameToCanonical.get(name) ?? name;
897
+ };
731
898
  for (const route of routes) {
732
899
  const names = generateRouteSchemaNames(route);
733
900
  const pathParams = route.parameters.filter((p) => p.in === "path");
@@ -742,16 +909,20 @@ function generateRequestResponseObjects(routes) {
742
909
  }
743
910
  const requestParts = [];
744
911
  if (names.paramsSchemaName && pathParams.length > 0) {
745
- requestParts.push(`params: ${names.paramsSchemaName}`);
912
+ requestParts.push(
913
+ `params: ${resolveSchemaName(names.paramsSchemaName)}`
914
+ );
746
915
  }
747
916
  if (names.querySchemaName && queryParams.length > 0) {
748
- requestParts.push(`query: ${names.querySchemaName}`);
917
+ requestParts.push(`query: ${resolveSchemaName(names.querySchemaName)}`);
749
918
  }
750
919
  if (names.headersSchemaName && headerParams.length > 0) {
751
- requestParts.push(`headers: ${names.headersSchemaName}`);
920
+ requestParts.push(
921
+ `headers: ${resolveSchemaName(names.headersSchemaName)}`
922
+ );
752
923
  }
753
924
  if (names.bodySchemaName && route.requestBody) {
754
- requestParts.push(`body: ${names.bodySchemaName}`);
925
+ requestParts.push(`body: ${resolveSchemaName(names.bodySchemaName)}`);
755
926
  }
756
927
  if (requestParts.length > 0) {
757
928
  requestMethodObj[route.method] = requestParts;
@@ -774,7 +945,7 @@ function generateRequestResponseObjects(routes) {
774
945
  route.method,
775
946
  suffix
776
947
  );
777
- responseMethodObj[route.method][statusCode] = responseSchemaName;
948
+ responseMethodObj[route.method][statusCode] = resolveSchemaName(responseSchemaName);
778
949
  }
779
950
  }
780
951
  lines.push("export const Request = {");
@@ -814,6 +985,25 @@ function generateRequestResponseObjects(routes) {
814
985
  lines.push("} as const;");
815
986
  return lines;
816
987
  }
988
+ function collectRouteSchemas(routes) {
989
+ const collected = [];
990
+ for (const route of routes) {
991
+ for (const [statusCode, responseSchema] of Object.entries(
992
+ route.responses
993
+ )) {
994
+ if (!responseSchema) continue;
995
+ const isSuccess = statusCode.startsWith("2");
996
+ const suffix = isSuccess ? `${statusCode}Response` : `${statusCode}ErrorResponse`;
997
+ const responseSchemaName = generateRouteSchemaName2(
998
+ route.path,
999
+ route.method,
1000
+ suffix
1001
+ );
1002
+ collected.push({ name: responseSchemaName, schema: responseSchema });
1003
+ }
1004
+ }
1005
+ return collected;
1006
+ }
817
1007
  var openApiToZodTsCode = (openapi, customImportLines, options) => {
818
1008
  const components = openapi["components"];
819
1009
  const schemas = components?.["schemas"] ?? {};
@@ -826,6 +1016,7 @@ var openApiToZodTsCode = (openapi, customImportLines, options) => {
826
1016
  lines.push("import { z } from 'zod';");
827
1017
  lines.push(...customImportLines ?? []);
828
1018
  lines.push("");
1019
+ const registry = createSchemaRegistry();
829
1020
  const sortedSchemaNames = topologicalSortSchemas(schemas);
830
1021
  for (const name of sortedSchemaNames) {
831
1022
  const schema = schemas[name];
@@ -836,19 +1027,37 @@ var openApiToZodTsCode = (openapi, customImportLines, options) => {
836
1027
  lines.push(`export const ${schemaName} = ${zodExpr};`);
837
1028
  lines.push(`export type ${typeName} = z.infer<typeof ${schemaName}>;`);
838
1029
  lines.push("");
1030
+ const fingerprint = getSchemaFingerprint(schema);
1031
+ preRegisterSchema(registry, schemaName, fingerprint);
839
1032
  }
840
1033
  }
841
1034
  if (options?.includeRoutes) {
842
1035
  const routes = parseOpenApiPaths(openapi);
843
1036
  if (routes.length > 0) {
844
- const routeSchemas = generateRouteSchemas(
1037
+ const routeSchemaList = collectRouteSchemas(routes);
1038
+ const commonSchemas = findCommonSchemas(routeSchemaList, 2);
1039
+ if (commonSchemas.length > 0) {
1040
+ lines.push("// Common Error Schemas (deduplicated)");
1041
+ for (const common of commonSchemas) {
1042
+ const zodExpr = convertSchemaToZodString(common.schema);
1043
+ lines.push(`export const ${common.name} = ${zodExpr};`);
1044
+ preRegisterSchema(registry, common.name, common.fingerprint);
1045
+ }
1046
+ lines.push("");
1047
+ }
1048
+ const { declarations, schemaNameToCanonical } = generateRouteSchemas(
845
1049
  routes,
846
- convertSchemaToZodString
1050
+ convertSchemaToZodString,
1051
+ registry
847
1052
  );
848
- if (routeSchemas.length > 0) {
849
- lines.push(...routeSchemas);
1053
+ if (declarations.length > 0) {
1054
+ lines.push("// Route Schemas");
1055
+ lines.push(...declarations);
850
1056
  lines.push("");
851
- const requestResponseObjs = generateRequestResponseObjects(routes);
1057
+ const requestResponseObjs = generateRequestResponseObjects(
1058
+ routes,
1059
+ schemaNameToCanonical
1060
+ );
852
1061
  lines.push(...requestResponseObjs);
853
1062
  }
854
1063
  }