Repository: JuliaPy/pyjuliapkg

Issue:
Invalid uuid results in broken juliapkg.json file
juliapkg.add needs input validation or at least some cleanup. accidentally passed an incorrect object into the uuid field of add() and got a file looking like this:

![Image](https://github.com/user-attachments/assets/70efc74c-026f-4d1f-a077-d80316aa6828)

After which every juliapkg (and juliacall) function would break as the file has malformed json.

Hints from issue comments:

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3db415d..b17f7bd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 ## Unreleased
 * Support editable dependencies from setuptools (experimental).
+* Better input validation.
 
 ## v0.1.17 (2025-05-13)
 * Respect `JULIAUP_DEPOT_PATH` when searching for Julia using juliaup.
diff --git a/src/juliapkg/deps.py b/src/juliapkg/deps.py
index 0bb0fb2..fcde17d 100644
--- a/src/juliapkg/deps.py
+++ b/src/juliapkg/deps.py
@@ -4,6 +4,7 @@ import logging
 import os
 import sys
 from subprocess import run
+from typing import Union
 
 from filelock import FileLock
 
@@ -50,22 +51,53 @@ def save_meta(meta):
 class PkgSpec:
     def __init__(
         self,
-        name,
-        uuid,
-        dev=False,
-        version=None,
-        path=None,
-        subdir=None,
-        url=None,
-        rev=None,
+        name: str,
+        uuid: str,
+        dev: bool = False,
+        version: Union[str, Version, None] = None,
+        path: Union[str, None] = None,
+        subdir: Union[str, None] = None,
+        url: Union[str, None] = None,
+        rev: Union[str, None] = None,
     ):
+        # Validate name (non-empty string)
+        if not isinstance(name, str) or not name.strip():
+            raise ValueError("name must be a non-empty string")
         self.name = name
+
+        # Validate UUID (non-empty string, could add more specific UUID validation)
+        if not isinstance(uuid, str) or not uuid.strip():
+            raise ValueError("uuid must be a non-empty string")
         self.uuid = uuid
+
+        # Validate dev (boolean)
+        if not isinstance(dev, bool):
+            raise TypeError("dev must be a boolean")
         self.dev = dev
+
+        # Validate version (string, Version, or None)
+        if version is not None and not isinstance(version, (str, Version)):
+            raise TypeError("version must be a string, Version, or None")
         self.version = version
+
+        # Validate path (string or None)
+        if path is not None and not isinstance(path, str):
+            raise TypeError("path must be a string or None")
         self.path = path
+
+        # Validate subdir (string or None)
+        if subdir is not None and not isinstance(subdir, str):
+            raise TypeError("subdir must be a string or None")
         self.subdir = subdir
+
+        # Validate url (string or None)
+        if url is not None and not isinstance(url, str):
+            raise TypeError("url must be a string or None")
         self.url = url
+
+        # Validate rev (string or None)
+        if rev is not None and not isinstance(rev, str):
+            raise TypeError("rev must be a string or None")
         self.rev = rev
 
     def jlstr(self):


Test patch:
diff --git a/test/test_internals.py b/test/test_internals.py
index 0b1c05c..d6f0975 100644
--- a/test/test_internals.py
+++ b/test/test_internals.py
@@ -1,4 +1,7 @@
+import pytest
+
 import juliapkg
+from juliapkg.deps import PkgSpec
 
 
 def test_openssl_compat():
@@ -8,3 +11,70 @@ def test_openssl_compat():
     assert juliapkg.deps.openssl_compat((3, 1, 0)) == "3 - 3.1"
     assert juliapkg.deps.openssl_compat((3, 1, 2)) == "3 - 3.1"
     assert isinstance(juliapkg.deps.openssl_compat(), str)
+
+
+def test_pkgspec_validation():
+    # Test valid construction
+    spec = PkgSpec(name="Example", uuid="123e4567-e89b-12d3-a456-426614174000")
+    assert spec.name == "Example"
+    assert spec.uuid == "123e4567-e89b-12d3-a456-426614174000"
+    assert spec.dev is False
+    assert spec.version is None
+    assert spec.path is None
+    assert spec.subdir is None
+    assert spec.url is None
+    assert spec.rev is None
+
+    # Test with all parameters
+    spec = PkgSpec(
+        name="Example",
+        uuid="123e4567-e89b-12d3-a456-426614174000",
+        dev=True,
+        version="1.0.0",
+        path="/path/to/pkg",
+        subdir="subdir",
+        url="https://example.com/pkg.git",
+        rev="main",
+    )
+    assert spec.dev is True
+    assert spec.version == "1.0.0"
+    assert spec.path == "/path/to/pkg"
+    assert spec.subdir == "subdir"
+    assert spec.url == "https://example.com/pkg.git"
+    assert spec.rev == "main"
+
+    # Test invalid name
+    with pytest.raises(ValueError, match="name must be a non-empty string"):
+        PkgSpec(name="", uuid="0000")
+    with pytest.raises(ValueError, match="name must be a non-empty string"):
+        PkgSpec(name=123, uuid="0000")
+
+    # Test invalid UUID
+    with pytest.raises(ValueError, match="uuid must be a non-empty string"):
+        PkgSpec(name="Example", uuid="")
+    with pytest.raises(ValueError, match="uuid must be a non-empty string"):
+        PkgSpec(name="Example", uuid=123)
+
+    # Test invalid dev flag
+    with pytest.raises(TypeError, match="dev must be a boolean"):
+        PkgSpec(name="Example", uuid="0000", dev="not-a-boolean")
+
+    # Test invalid version type
+    with pytest.raises(TypeError, match="version must be a string, Version, or None"):
+        PkgSpec(name="Example", uuid="0000", version=123)
+
+    # Test invalid path type
+    with pytest.raises(TypeError, match="path must be a string or None"):
+        PkgSpec(name="Example", uuid="0000", path=123)
+
+    # Test invalid subdir type
+    with pytest.raises(TypeError, match="subdir must be a string or None"):
+        PkgSpec(name="Example", uuid="0000", subdir=123)
+
+    # Test invalid url type
+    with pytest.raises(TypeError, match="url must be a string or None"):
+        PkgSpec(name="Example", uuid="0000", url=123)
+
+    # Test invalid rev type
+    with pytest.raises(TypeError, match="rev must be a string or None"):
+        PkgSpec(name="Example", uuid="0000", rev=123)