Repository: fabien0102/ts-to-zod
Pull request #135 (2023-05-23 14:52:25)

Add import handling for external classes
# Why

This PR adds support for imports.

The following input: 
```ts
// source.ts
import { Person } from "@3rdparty/person";

export interface Hero {
  name: string;
  realPerson: Person;
}
```

will output

```ts
//output.ts

const personSchema = z.any();

export const heroSchema = z.object({
  name: z.string(),
  realPerson: personSchema,
});
```


codecov-commenter: ## [Codecov](https://app.codecov.io/gh/fabien0102/ts-to-zod/pull/135?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Fabien+BERNARD) Report
> Merging [#135](https://app.codecov.io/gh/fabien0102/ts-to-zod/pull/135?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Fabien+BERNARD) (a6bf5e4) into [main](https://app.codecov.io/gh/fabien0102/ts-to-zod/commit/9cb8a3e1100a68fb3d33cbe40e72f08504f11fd5?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Fabien+BERNARD) (9cb8a3e) will **increase** coverage by `1.10%`.
> The diff coverage is `97.91%`.

:exclamation: Your organization is not using the GitHub App Integration. As a result you may experience degraded service beginning May 15th. Please [install the Github App Integration](https://github.com/apps/codecov) for your organization. [Read more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Fabien+BERNARD).

```diff
@@            Coverage Diff             @@
##             main     #135      +/-   ##
==========================================
+ Coverage   96.04%   97.14%   +1.10%     
==========================================
  Files          13       14       +1     
  Lines         657      700      +43     
  Branches      265      285      +20     
==========================================
+ Hits          631      680      +49     
+ Misses         26       20       -6     
```


| [Impacted Files](https://app.codecov.io/gh/fabien0102/ts-to-zod/pull/135?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Fabien+BERNARD) | Coverage Δ | |
|---|---|---|
| [src/core/generateZodSchema.ts](https://app.codecov.io/gh/fabien0102/ts-to-zod/pull/135?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Fabien+BERNARD#diff-c3JjL2NvcmUvZ2VuZXJhdGVab2RTY2hlbWEudHM=) | `95.88% <75.00%> (-0.32%)` | :arrow_down: |
| [src/core/generate.ts](https://app.codecov.io/gh/fabien0102/ts-to-zod/pull/135?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Fabien+BERNARD#diff-c3JjL2NvcmUvZ2VuZXJhdGUudHM=) | `96.74% <100.00%> (+0.63%)` | :arrow_up: |
| [src/utils/importHandling.ts](https://app.codecov.io/gh/fabien0102/ts-to-zod/pull/135?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Fabien+BERNARD#diff-c3JjL3V0aWxzL2ltcG9ydEhhbmRsaW5nLnRz) | `100.00% <100.00%> (ø)` | |

... and [1 file with indirect coverage changes](https://app.codecov.io/gh/fabien0102/ts-to-zod/pull/135/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Fabien+BERNARD)


tvillaren: Blocked by https://github.com/fabien0102/ts-to-zod/issues/140
tvillaren: Hey @fabien0102, 

Thank you for your recent update & merge. 
Following those, here is proposal for a feature addition to handle non-relative imports. I already use it today (the only missing bit to ship it was the validation issue #140). 

I acknowledge that this feature is a bit opinionated as it takes any non-relative import as not being a zod schema. 
Happy to exchange about it here. 

As a follow-up, I see an opening to handle relative imports as zod schemas (generated or not) and may open up the door to multi-file generation support eventually.



dan-blank: Thanks for the PR!

> I acknowledge that this feature is a bit opinionated as it takes any non-relative import as not being a zod schema. Happy to exchange about it here.

I think this is a sensible default. One option came to my mind:

Use a comment to tell ts-to-zod that the import is assumed to be a zod schema.

```TypeScript
// source.ts

import { Person } from "@3rdparty/person";

// ts-to-zod ImportIsZodSchema Person

export interface Hero {
  name: string;
  realPerson: Person;
}
```

Its semantics would be:
- Once we parsed `// ts-to-zod ImportsIsZodSchema Person`, we assume as every use site of Person that Person is a zod schema
- Does not work for `* ` in imports. When encountering `*`, log an error ("Only works for explicit imports") and use the default (= assume that the import is not a zod schema)
- Does not work for `as` in imports

> It works for non-relative modules only, based on the principle that we might want to handle relative modules using the ts-to-zod conversion.

I don't understand the reasoning, could you explain/clarify in your OP? :)
tvillaren: The rationale behind using only non-relative imports for this feature was mostly based on the rationale that 3rd party imports had very little chance to be Zod Schema.

Relative imports, on the other hand, have maybe a higher chance to be a class / type / interface that is also transformed somewhere in the codebase into a zodSchema.
👉 For those, there are different handling strategies (like using JSTags-style comments) to tell the transpiler we have a zodSchema for this somewhere
👉 And I had as a future step to maybe fiddle with multiple file ts-to-zod (that would be based on relative imports obviously)

👉 **In the meantime, it's quite easy to handle any imports as 3rd party ones and have specific JSTags comments in the future to handle zod schema (generated or referenced manually)** 
tvillaren: Hello @dan-blank, @fabien0102 👋 

I updated this PR to allow any type of imports (non-relative *and* relative modules alltogether). 
For now, it assumes that imports are to be used as `z.instanceof()`, I'm going to work on another PR to add JSTags for import statement to allow specifying when we reference a zod schema.

WDYT? 
anthony-dandrea: Looks good here as well! Ran into the same small issue mentioned in your other [PR](https://github.com/fabien0102/ts-to-zod/pull/148#issuecomment-1664231249).
tvillaren: Hello @fabien0102 , @schiller-manuel 

After much discussions and thoughts, I reworked this Pull Request to support `imports` and generate a `z.any()` for all type references coming from an import. 

It was previously using `z.instanceof(ImportName)` but it was an overlook from my side of the [official documentation](https://zod.dev/?id=instanceof) where `instanceof` is meant to be used only for imported `Class` (not types or interfaces).

Additionally, I had to update the `fixOptionalAny` bit that was added a couple of months ago to support type references as well. 

But now this supports simply the imports, and I will rebase https://github.com/fabien0102/ts-to-zod/pull/148 to support imports of Zod Schemas as well.
tvillaren: Hey @schiller-manuel, 

Thank you for you review, I just cleaned the two commented lines. 
I'm working on https://github.com/fabien0102/ts-to-zod/pull/148 and will push an update ASAP.

Thanks.

Patch:
diff --git a/README.md b/README.md
index 5b33998..3046b96 100644
--- a/README.md
+++ b/README.md
@@ -239,7 +239,7 @@ module.exports = [
     name: "example",
     input: "example/heros.ts",
     output: "example/heros.zod.ts",
-    jsDocTagFilter: (tags) => tags.map(tag => tag.name).includes("toExtract") // <= rule here
+    jsDocTagFilter: (tags) => tags.map((tag) => tag.name).includes("toExtract"), // <= rule here
   },
 ];
 
@@ -388,6 +388,33 @@ export const heroSchema = z.object({
 });
 ```
 
+It can also reference imported types from any modules but will use an `any` validation as placeholder, as we cannot now
+their schema.
+
+```ts
+// source.ts
+import { Person } from "@3rdparty/person";
+import { Villain } from "./villain";
+
+export interface Hero {
+  name: string;
+  realPerson: Person;
+  nemesis: Villain;
+}
+
+// output.ts
+
+const personSchema = z.any();
+
+const villainSchema = z.any();
+
+export const heroSchema = z.object({
+  name: z.string(),
+  realPerson: personSchema,
+  nemesis: villainSchema;
+});
+```
+
 ## Programmatic API
 
 You need more than one file? Want even more power? No problem, just use the tool as a library.
diff --git a/src/core/generate.ts b/src/core/generate.ts
index 115ede9..e909edf 100644
--- a/src/core/generate.ts
+++ b/src/core/generate.ts
@@ -9,9 +9,14 @@ import {
   isTypeNode,
   TypeNode,
 } from "../utils/traverseTypes";
+
+import { getImportIdentifiers } from "../utils/importHandling";
 import { generateIntegrationTests } from "./generateIntegrationTests";
 import { generateZodInferredType } from "./generateZodInferredType";
-import { generateZodSchemaVariableStatement } from "./generateZodSchema";
+import {
+  generateZodSchemaVariableStatement,
+  generateZodSchemaVariableStatementForImport,
+} from "./generateZodSchema";
 import { transformRecursiveSchema } from "./transformRecursiveSchema";
 
 export interface GenerateProps {
@@ -82,13 +87,22 @@ export function generate({
   // declare a map to store the interface name and its corresponding zod schema
   const typeNameMapping = new Map<string, TypeNode>();
 
+  // handling existing import statements
+  const importNamesAvailable = new Set<string>();
+  const importNamesUsed: string[] = [];
+
   const typesNeedToBeExtracted = new Set<string>();
 
   const typeNameMapBuilder = (node: ts.Node) => {
     if (isTypeNode(node)) {
       typeNameMapping.set(node.name.text, node);
     }
+    if (ts.isImportDeclaration(node) && node.importClause) {
+      const imports = getImportIdentifiers(node);
+      imports.forEach((i) => importNamesAvailable.add(i));
+    }
   };
+
   ts.forEachChild(sourceFile, typeNameMapBuilder);
   const visitor = (node: ts.Node) => {
     if (
@@ -113,11 +127,15 @@ export function generate({
     const node = typeNameMapping.get(typeName);
     if (node) {
       nodes.push(node);
+    } else {
+      if (importNamesAvailable.has(typeName)) {
+        importNamesUsed.push(typeName);
+      }
     }
   });
 
-  // Generate zod schemas
-  const zodSchemas = nodes.map((node) => {
+  // Generate zod schemas for type nodes
+  const zodTypeSchemas = nodes.map((node) => {
     const typeName = node.name.text;
     const varName = getSchemaName(typeName);
     const zodSchema = generateZodSchemaVariableStatement({
@@ -132,6 +150,23 @@ export function generate({
 
     return { typeName, varName, ...zodSchema };
   });
+
+  // Generate zod schemas for imports
+  const zodImportSchemas = importNamesUsed.map((importName) => {
+    const varName = getSchemaName(importName);
+    return {
+      dependencies: [],
+      statement: generateZodSchemaVariableStatementForImport({
+        varName,
+        zodImportValue: "z",
+      }),
+      requiresImport: false,
+      typeName: importName,
+      varName,
+    };
+  });
+
+  const zodSchemas = zodTypeSchemas.concat(zodImportSchemas);
   const zodSchemaNames = zodSchemas.map(({ varName }) => varName);
 
   // Resolves statements order
@@ -234,14 +269,14 @@ ${Array.from(zodSchemasWithMissingDependencies).join("\n")}`
 
   const transformedSourceText = printerWithComments.printFile(sourceFile);
 
-  const imports = Array.from(typeImports.values());
+  const typeImportsValues = Array.from(typeImports.values());
   const getZodSchemasFile = (
     typesImportPath: string
   ) => `// Generated by ts-to-zod
 import { z } from "zod";
 ${
-  imports.length
-    ? `import { ${imports.join(", ")} } from "${typesImportPath}";\n`
+  typeImportsValues.length
+    ? `import { ${typeImportsValues.join(", ")} } from "${typesImportPath}";\n`
     : ""
 }
 ${Array.from(statements.values())
@@ -346,7 +381,7 @@ ${Array.from(statements.values())
     /**
      * `true` if zodSchemaFile have some resolvable circular dependencies
      */
-    hasCircularDependencies: imports.length > 0,
+    hasCircularDependencies: typeImportsValues.length > 0,
   };
 }
 
diff --git a/src/core/generateZodSchema.ts b/src/core/generateZodSchema.ts
index 440ee66..be4c1c7 100644
--- a/src/core/generateZodSchema.ts
+++ b/src/core/generateZodSchema.ts
@@ -174,6 +174,38 @@ export function generateZodSchemaVariableStatement({
   };
 }
 
+/**
+ * Generate zod schema declaration for imported types (using any)
+ *
+ * ```ts
+ * const ${varName} = ${zodImportValue}.any()
+ * ```
+ */
+export function generateZodSchemaVariableStatementForImport({
+  varName,
+  zodImportValue = "z",
+}: {
+  varName: string;
+  zodImportValue?: string;
+}) {
+  const schema = buildZodSchema(zodImportValue, "any");
+
+  return f.createVariableStatement(
+    undefined, // No modifier expected
+    f.createVariableDeclarationList(
+      [
+        f.createVariableDeclaration(
+          f.createIdentifier(varName),
+          undefined,
+          undefined,
+          schema
+        ),
+      ],
+      ts.NodeFlags.Const
+    )
+  );
+}
+
 function buildZodProperties({
   members,
   zodImportValue: z,
diff --git a/src/utils/fixOptionalAny.ts b/src/utils/fixOptionalAny.ts
index 83552bb..2131cee 100644
--- a/src/utils/fixOptionalAny.ts
+++ b/src/utils/fixOptionalAny.ts
@@ -17,18 +17,25 @@ export function fixOptionalAny(sourceText: string) {
     const visit: ts.Visitor = (node) => {
       node = ts.visitEachChild(node, visit, context);
 
-      if (
-        ts.isPropertySignature(node) &&
-        node.type?.kind === ts.SyntaxKind.AnyKeyword
-      ) {
-        return ts.factory.createPropertySignature(
-          node.modifiers,
-          node.name,
-          f.createToken(ts.SyntaxKind.QuestionToken), // Add `questionToken`
-          node.type
-        );
-      }
+      if (ts.isPropertySignature(node) && node.type) {
+        const typeNode = node.type;
+
+        if (shouldAddQuestionToken(typeNode)) {
+          return createOptionalPropertyNode(node);
+        }
 
+        // Handling nested Any / TypeReference
+        if (
+          ts.isUnionTypeNode(typeNode) ||
+          ts.isIntersectionTypeNode(typeNode)
+        ) {
+          const withQuestionToken = typeNode.types.filter((childNode) =>
+            shouldAddQuestionToken(childNode)
+          );
+          if (withQuestionToken.length > 0)
+            return createOptionalPropertyNode(node);
+        }
+      }
       return node;
     };
 
@@ -39,3 +46,16 @@ export function fixOptionalAny(sourceText: string) {
 
   return printer.printFile(outputFile.transformed[0]);
 }
+
+function shouldAddQuestionToken(node: ts.TypeNode) {
+  return node.kind === ts.SyntaxKind.AnyKeyword || ts.isTypeReferenceNode(node);
+}
+
+function createOptionalPropertyNode(node: ts.PropertySignature) {
+  return ts.factory.createPropertySignature(
+    node.modifiers,
+    node.name,
+    f.createToken(ts.SyntaxKind.QuestionToken), // Add `questionToken`
+    node.type
+  );
+}
diff --git a/src/utils/importHandling.ts b/src/utils/importHandling.ts
new file mode 100644
index 0000000..132f0e4
--- /dev/null
+++ b/src/utils/importHandling.ts
@@ -0,0 +1,34 @@
+import ts from "typescript";
+
+/**
+ * Extracts the list of import identifiers from an import clause
+ * @param node an ImportDeclaration node
+ * @returns an array of all identifiers found in statement
+ */
+export function getImportIdentifiers(node: ts.ImportDeclaration): string[] {
+  if (!node.importClause) return [];
+
+  const { importClause } = node;
+  const importIdentifiers: string[] = [];
+
+  // Case `import MyGlobal from "module";`
+  if (importClause.name) importIdentifiers.push(importClause.name.text);
+
+  if (importClause.namedBindings) {
+    // Cases `import { A, B } from "module"`
+    // and `import C from "module"`
+    if (ts.isNamedImports(importClause.namedBindings)) {
+      for (const element of importClause.namedBindings.elements) {
+        if (ts.isImportSpecifier(element)) {
+          importIdentifiers.push(element.name.text);
+        }
+      }
+    }
+    // Case `import * as A from "module"`
+    else if (ts.isNamespaceImport(importClause.namedBindings)) {
+      importIdentifiers.push(importClause.namedBindings.name.text);
+    }
+  }
+
+  return importIdentifiers;
+}


Test patch:
diff --git a/src/core/generate.test.ts b/src/core/generate.test.ts
index 500c6df..7a3c576 100644
--- a/src/core/generate.test.ts
+++ b/src/core/generate.test.ts
@@ -526,4 +526,200 @@ describe("generate", () => {
       `);
     });
   });
+
+  describe("with import statements", () => {
+    describe("single import", () => {
+      const sourceText = `
+      import Villain from "@project/villain-module";
+  
+      export interface Superman {
+        nemesis: Villain;
+        id: number
+      }
+      `;
+
+      const { getZodSchemasFile, getIntegrationTestFile, errors } = generate({
+        sourceText,
+      });
+
+      it("should generate the zod schemas", () => {
+        expect(getZodSchemasFile("./source")).toMatchInlineSnapshot(`
+      "// Generated by ts-to-zod
+      import { z } from "zod";
+
+      const villainSchema = z.any();
+
+      export const supermanSchema = z.object({
+          nemesis: villainSchema,
+          id: z.number()
+      });
+      "
+      `);
+      });
+
+      it("should generate the integration tests", () => {
+        expect(getIntegrationTestFile("./hero", "hero.zod"))
+          .toMatchInlineSnapshot(`
+      "// Generated by ts-to-zod
+      import { z } from "zod";
+
+      import * as spec from "./hero";
+      import * as generated from "hero.zod";
+
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      function expectType<T>(_: T) {
+        /* noop */
+      }
+
+      export type supermanSchemaInferredType = z.infer<typeof generated.supermanSchema>;
+
+      expectType<spec.Superman>({} as supermanSchemaInferredType)
+      expectType<supermanSchemaInferredType>({} as spec.Superman)
+      "
+      `);
+      });
+
+      it("should not have any errors", () => {
+        expect(errors.length).toBe(0);
+      });
+    });
+
+    describe("multiple imports", () => {
+      const sourceText = `
+      import { Name } from "nameModule";
+      import * as Person from "person-module";
+      import Villain from "@project/villain-module";
+  
+      export interface Superman {
+        name: Name;
+        person: Person;
+        nemesis: Villain;
+        id: number
+      }
+      `;
+
+      const { getZodSchemasFile } = generate({
+        sourceText,
+      });
+
+      it("should generate the zod schemas", () => {
+        expect(getZodSchemasFile("./source")).toMatchInlineSnapshot(`
+      "// Generated by ts-to-zod
+      import { z } from "zod";
+
+      const nameSchema = z.any();
+
+      const personSchema = z.any();
+
+      const villainSchema = z.any();
+
+      export const supermanSchema = z.object({
+          name: nameSchema,
+          person: personSchema,
+          nemesis: villainSchema,
+          id: z.number()
+      });
+      "
+      `);
+      });
+    });
+
+    describe("multiple imports from single module", () => {
+      const sourceText = `
+      import { Name, Person, Villain } from "./module";
+  
+      export interface Superman {
+        name: Name;
+        person: Person;
+        nemesis: Villain;
+        id: number
+      }
+      `;
+
+      const { getZodSchemasFile } = generate({
+        sourceText,
+      });
+
+      it("should generate the zod schemas", () => {
+        expect(getZodSchemasFile("./source")).toMatchInlineSnapshot(`
+      "// Generated by ts-to-zod
+      import { z } from "zod";
+
+      const nameSchema = z.any();
+
+      const personSchema = z.any();
+
+      const villainSchema = z.any();
+
+      export const supermanSchema = z.object({
+          name: nameSchema,
+          person: personSchema,
+          nemesis: villainSchema,
+          id: z.number()
+      });
+      "
+      `);
+      });
+    });
+
+    describe("import as extend", () => {
+      const sourceText = `
+      import * as Person from "person-module";
+  
+      export interface Superman extends Person{
+        id: number
+      }
+      `;
+
+      const { getZodSchemasFile } = generate({
+        sourceText,
+      });
+
+      it("should generate the zod schemas", () => {
+        expect(getZodSchemasFile("./source")).toMatchInlineSnapshot(`
+      "// Generated by ts-to-zod
+      import { z } from "zod";
+
+      const personSchema = z.any();
+
+      export const supermanSchema = personSchema.extend({
+          id: z.number()
+      });
+      "
+      `);
+      });
+    });
+
+    describe("import as union", () => {
+      const sourceText = `
+      import { Hero, Villain } from "module"
+    
+      export interface Person {
+        id: number
+        type: Hero | Villain
+      }
+      `;
+
+      const { getZodSchemasFile } = generate({
+        sourceText,
+      });
+
+      it("should generate the zod schemas", () => {
+        expect(getZodSchemasFile("./source")).toMatchInlineSnapshot(`
+        "// Generated by ts-to-zod
+        import { z } from "zod";
+
+        const heroSchema = z.any();
+
+        const villainSchema = z.any();
+
+        export const personSchema = z.object({
+            id: z.number(),
+            type: z.union([heroSchema, villainSchema])
+        });
+        "
+      `);
+      });
+    });
+  });
 });
diff --git a/src/core/validateGeneratedTypes.test.ts b/src/core/validateGeneratedTypes.test.ts
index d20549f..6200110 100644
--- a/src/core/validateGeneratedTypes.test.ts
+++ b/src/core/validateGeneratedTypes.test.ts
@@ -47,6 +47,367 @@ describe("validateGeneratedTypes", () => {
     expect(errors).toEqual([]);
   });
 
+  it("should return no error if we use a non-optional any", () => {
+    const sourceTypes = {
+      sourceText: `
+      export interface Citizen {
+        villain: any;
+      };
+    `,
+      relativePath: "source.ts",
+    };
+
+    const zodSchemas = {
+      sourceText: `// Generated by ts-to-zod
+    import { z } from "zod";
+    export const citizenSchema = z.object({
+      villain: z.any()
+    });
+    `,
+      relativePath: "source.zod.ts",
+    };
+
+    const integrationTests = {
+      sourceText: `// Generated by ts-to-zod
+      import { z } from "zod";
+
+      import * as spec from "./${sourceTypes.relativePath.slice(0, -3)}";
+      import * as generated from "./${zodSchemas.relativePath.slice(0, -3)}";
+
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      function expectType<T>(_: T) {
+        /* noop */
+      }
+
+      export type CitizenInferredType = z.infer<typeof generated.citizenSchema>;
+
+      expectType<CitizenInferredType>({} as spec.Citizen);
+      expectType<spec.Citizen>({} as CitizenInferredType);
+  `,
+      relativePath: "source.integration.ts",
+    };
+
+    const errors = validateGeneratedTypes({
+      sourceTypes,
+      zodSchemas,
+      integrationTests,
+      skipParseJSDoc: false,
+    });
+
+    expect(errors).toEqual([]);
+  });
+
+  it("should return no error if we use a 'deep' non-optional any", () => {
+    const sourceTypes = {
+      sourceText: `
+      export interface Citizen {
+        villain: {
+          name: string
+          id: any
+        };
+      };
+    `,
+      relativePath: "source.ts",
+    };
+
+    const zodSchemas = {
+      sourceText: `// Generated by ts-to-zod
+    import { z } from "zod";
+    export const citizenSchema = z.object({
+      villain: z.object({
+        name: z.string(),
+        id: z.any()
+      })
+    });
+    `,
+      relativePath: "source.zod.ts",
+    };
+
+    const integrationTests = {
+      sourceText: `// Generated by ts-to-zod
+      import { z } from "zod";
+
+      import * as spec from "./${sourceTypes.relativePath.slice(0, -3)}";
+      import * as generated from "./${zodSchemas.relativePath.slice(0, -3)}";
+
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      function expectType<T>(_: T) {
+        /* noop */
+      }
+
+      export type CitizenInferredType = z.infer<typeof generated.citizenSchema>;
+
+      expectType<CitizenInferredType>({} as spec.Citizen);
+      expectType<spec.Citizen>({} as CitizenInferredType);
+  `,
+      relativePath: "source.integration.ts",
+    };
+
+    const errors = validateGeneratedTypes({
+      sourceTypes,
+      zodSchemas,
+      integrationTests,
+      skipParseJSDoc: false,
+    });
+
+    expect(errors).toEqual([]);
+  });
+
+  it("should return no error if we use an external import", () => {
+    const sourceTypes = {
+      sourceText: `
+      import { Hero } from "./hero-module.ts"
+
+      export interface Citizen {
+        hero: Hero
+      };
+    `,
+      relativePath: "source.ts",
+    };
+
+    const zodSchemas = {
+      sourceText: `// Generated by ts-to-zod
+    import { z } from "zod";
+    const heroSchema = z.any();
+
+    export const citizenSchema = z.object({
+      hero: heroSchema
+    });
+    `,
+      relativePath: "source.zod.ts",
+    };
+
+    const integrationTests = {
+      sourceText: `// Generated by ts-to-zod
+      import { z } from "zod";
+
+      import * as spec from "./${sourceTypes.relativePath.slice(0, -3)}";
+      import * as generated from "./${zodSchemas.relativePath.slice(0, -3)}";
+
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      function expectType<T>(_: T) {
+        /* noop */
+      }
+
+      export type CitizenInferredType = z.infer<typeof generated.citizenSchema>;
+
+      expectType<CitizenInferredType>({} as spec.Citizen);
+      expectType<spec.Citizen>({} as CitizenInferredType);
+  `,
+      relativePath: "source.integration.ts",
+    };
+
+    const errors = validateGeneratedTypes({
+      sourceTypes,
+      zodSchemas,
+      integrationTests,
+      skipParseJSDoc: false,
+    });
+
+    expect(errors).toEqual([]);
+  });
+
+  it("should return no error if we use a deep external import", () => {
+    const sourceTypes = {
+      sourceText: `
+      import { Villain } from "./villain-module.ts"
+      
+      import { Hero } from "./hero-module.ts"
+
+      export interface Citizen {
+        villain: Villain
+        heroData: {
+          name: string
+          hero: Hero
+        }
+      };
+    `,
+      relativePath: "source.ts",
+    };
+
+    const zodSchemas = {
+      sourceText: `// Generated by ts-to-zod
+    import { z } from "zod";
+
+    const villainSchema = z.any();
+    
+    const heroSchema = z.any();
+
+    export const citizenSchema = z.object({
+      villain: villainSchema
+      heroData: z.object({
+        name: z.string(),
+        hero: heroSchema
+      })
+    });
+    `,
+      relativePath: "source.zod.ts",
+    };
+
+    const integrationTests = {
+      sourceText: `// Generated by ts-to-zod
+      import { z } from "zod";
+
+      import * as spec from "./${sourceTypes.relativePath.slice(0, -3)}";
+      import * as generated from "./${zodSchemas.relativePath.slice(0, -3)}";
+
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      function expectType<T>(_: T) {
+        /* noop */
+      }
+
+      export type CitizenInferredType = z.infer<typeof generated.citizenSchema>;
+
+      expectType<CitizenInferredType>({} as spec.Citizen);
+      expectType<spec.Citizen>({} as CitizenInferredType);
+  `,
+      relativePath: "source.integration.ts",
+    };
+
+    const errors = validateGeneratedTypes({
+      sourceTypes,
+      zodSchemas,
+      integrationTests,
+      skipParseJSDoc: false,
+    });
+
+    expect(errors).toEqual([]);
+  });
+
+  it("should return no error if we use a deep external import with union", () => {
+    const sourceTypes = {
+      sourceText: `
+      import { Villain } from "./villain-module.ts"
+      
+      import { Hero } from "./hero-module.ts"
+
+      export interface Citizen {
+        villain: Villain
+        heroData: {
+          name: string
+          hero: Hero | string
+        }
+      };
+    `,
+      relativePath: "source.ts",
+    };
+
+    const zodSchemas = {
+      sourceText: `// Generated by ts-to-zod
+    import { z } from "zod";
+
+    const villainSchema = z.any();
+    
+    const heroSchema = z.any();
+
+    export const citizenSchema = z.object({
+      villain: villainSchema,
+      heroData: z.object({
+        name: z.string(),
+        hero: z.union([heroSchema, z.string()])
+      })
+    });
+    `,
+      relativePath: "source.zod.ts",
+    };
+
+    const integrationTests = {
+      sourceText: `// Generated by ts-to-zod
+      import { z } from "zod";
+
+      import * as spec from "./${sourceTypes.relativePath.slice(0, -3)}";
+      import * as generated from "./${zodSchemas.relativePath.slice(0, -3)}";
+
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      function expectType<T>(_: T) {
+        /* noop */
+      }
+
+      export type CitizenInferredType = z.infer<typeof generated.citizenSchema>;
+
+      expectType<CitizenInferredType>({} as spec.Citizen);
+      expectType<spec.Citizen>({} as CitizenInferredType);
+  `,
+      relativePath: "source.integration.ts",
+    };
+
+    const errors = validateGeneratedTypes({
+      sourceTypes,
+      zodSchemas,
+      integrationTests,
+      skipParseJSDoc: false,
+    });
+
+    expect(errors).toEqual([]);
+  });
+
+  it("should return no error if we use a deep external import with union", () => {
+    const sourceTypes = {
+      sourceText: `
+      import { Villain } from "./villain-module.ts"
+      
+      import { Hero } from "./hero-module.ts"
+
+      export interface Citizen {
+        villain: Villain
+        heroData: {
+          name: string
+          hero: { id:Hero } | string
+        }
+      };
+    `,
+      relativePath: "source.ts",
+    };
+
+    const zodSchemas = {
+      sourceText: `// Generated by ts-to-zod
+    import { z } from "zod";
+
+    const villainSchema = z.any();
+    
+    const heroSchema = z.any();
+
+    export const citizenSchema = z.object({
+      villain: villainSchema,
+      heroData: z.object({
+        name: z.string(),
+        hero: z.union([z.object({id: heroSchema}), z.string()])
+      })
+    });
+    `,
+      relativePath: "source.zod.ts",
+    };
+
+    const integrationTests = {
+      sourceText: `// Generated by ts-to-zod
+      import { z } from "zod";
+
+      import * as spec from "./${sourceTypes.relativePath.slice(0, -3)}";
+      import * as generated from "./${zodSchemas.relativePath.slice(0, -3)}";
+
+      // eslint-disable-next-line @typescript-eslint/no-unused-vars
+      function expectType<T>(_: T) {
+        /* noop */
+      }
+
+      export type CitizenInferredType = z.infer<typeof generated.citizenSchema>;
+
+      expectType<CitizenInferredType>({} as spec.Citizen);
+      expectType<spec.Citizen>({} as CitizenInferredType);
+  `,
+      relativePath: "source.integration.ts",
+    };
+
+    const errors = validateGeneratedTypes({
+      sourceTypes,
+      zodSchemas,
+      integrationTests,
+      skipParseJSDoc: false,
+    });
+
+    expect(errors).toEqual([]);
+  });
+
   it("should return an error if the types doesn't match", () => {
     const sourceTypes = {
       sourceText: `
diff --git a/src/utils/importHandling.test.ts b/src/utils/importHandling.test.ts
new file mode 100644
index 0000000..d65de71
--- /dev/null
+++ b/src/utils/importHandling.test.ts
@@ -0,0 +1,92 @@
+import ts from "typescript";
+import { findNode } from "./findNode";
+import { getImportIdentifiers } from "./importHandling";
+
+describe("getImportIdentifiers", () => {
+  it("should return nothing with a StringLiteral import", () => {
+    const sourceText = `
+        import "module";
+        `;
+
+    expect(getImportIdentifiers(getImportNode(sourceText))).toEqual([]);
+  });
+
+  it("should get the identifier from default", () => {
+    const sourceText = `
+    import MyGlobal from "module";
+    `;
+
+    expect(getImportIdentifiers(getImportNode(sourceText))).toEqual([
+      "MyGlobal",
+    ]);
+  });
+
+  it("should get the identifier with whole module aliased import", () => {
+    const sourceText = `
+    import * as MyGlobal from "module";
+    `;
+
+    expect(getImportIdentifiers(getImportNode(sourceText))).toEqual([
+      "MyGlobal",
+    ]);
+  });
+
+  it("should get the selected identifier", () => {
+    const sourceText = `
+    import { MyGlobal } from "module";
+    `;
+
+    expect(getImportIdentifiers(getImportNode(sourceText))).toEqual([
+      "MyGlobal",
+    ]);
+  });
+
+  it("should get the selected aliased identifier", () => {
+    const sourceText = `
+    import { AA as MyGlobal } from "module";
+    `;
+
+    expect(getImportIdentifiers(getImportNode(sourceText))).toEqual([
+      "MyGlobal",
+    ]);
+  });
+
+  it("should get the selected identifiers", () => {
+    const sourceText = `
+    import { MyGlobal, MyGlobal2 } from "module";
+    `;
+
+    expect(getImportIdentifiers(getImportNode(sourceText))).toEqual([
+      "MyGlobal",
+      "MyGlobal2",
+    ]);
+  });
+
+  it("should get the identifier from default, mixed with others", () => {
+    const sourceText = `
+    import MyGlobal, { MyGlobal2 } from "module";
+    `;
+
+    expect(getImportIdentifiers(getImportNode(sourceText))).toEqual([
+      "MyGlobal",
+      "MyGlobal2",
+    ]);
+  });
+});
+
+function getImportNode(sourceText: string): ts.ImportDeclaration {
+  const sourceFile = ts.createSourceFile(
+    "index.ts",
+    sourceText,
+    ts.ScriptTarget.Latest
+  );
+
+  const importNode = findNode(
+    sourceFile,
+    (node): node is ts.ImportDeclaration => ts.isImportDeclaration(node)
+  );
+  if (!importNode) {
+    throw new Error("No `type` or `interface` found!");
+  }
+  return importNode;
+}