Make Not expression JSON serializable (#2593)

#2520 

This PR makes the `Not` boolean expression in `pyiceberg` JSON
serializable using Pydantic

Please let me know if my approach or fix needs any improvements . I’m
open to feedback and happy to make changes based on suggestions.
Thank you !

---------

Co-authored-by: Fokko Driesprong <fokko@apache.org>
diff --git a/pyiceberg/expressions/__init__.py b/pyiceberg/expressions/__init__.py
index 8de5a28..0491a1f 100644
--- a/pyiceberg/expressions/__init__.py
+++ b/pyiceberg/expressions/__init__.py
@@ -350,12 +350,18 @@
         return (self.left, self.right)
 
 
-class Not(BooleanExpression):
+class Not(IcebergBaseModel, BooleanExpression):
     """NOT operation expression - logical negation."""
 
-    child: BooleanExpression
+    model_config = ConfigDict(arbitrary_types_allowed=True)
 
-    def __new__(cls, child: BooleanExpression) -> BooleanExpression:  # type: ignore
+    type: TypingLiteral["not"] = Field(default="not")
+    child: BooleanExpression = Field()
+
+    def __init__(self, child: BooleanExpression, **_: Any) -> None:
+        super().__init__(child=child)
+
+    def __new__(cls, child: BooleanExpression, **_: Any) -> BooleanExpression:  # type: ignore
         if child is AlwaysTrue():
             return AlwaysFalse()
         elif child is AlwaysFalse():
@@ -363,9 +369,12 @@
         elif isinstance(child, Not):
             return child.child
         obj = super().__new__(cls)
-        obj.child = child
         return obj
 
+    def __str__(self) -> str:
+        """Return the string representation of the Not class."""
+        return f"Not(child={self.child})"
+
     def __repr__(self) -> str:
         """Return the string representation of the Not class."""
         return f"Not(child={repr(self.child)})"
diff --git a/tests/expressions/test_expressions.py b/tests/expressions/test_expressions.py
index 45addd8..57a39e0 100644
--- a/tests/expressions/test_expressions.py
+++ b/tests/expressions/test_expressions.py
@@ -711,6 +711,12 @@
     assert not_ == pickle.loads(pickle.dumps(not_))
 
 
+def test_not_json_serialization_and_deserialization() -> None:
+    not_expr = Not(GreaterThan("a", 22))
+    json_str = not_expr.model_dump_json()
+    assert json_str == """{"type":"not","child":{"term":"a","type":"gt","value":22}}"""
+
+
 def test_always_true() -> None:
     always_true = AlwaysTrue()
     assert always_true.model_dump_json() == '"true"'