feat: Make `and` expression JSON serializable (#2784)

Related to: #2518, #2775 

# Rationale for this change

This work was done by @Aniketsy, I just opened this to get the tests
passing, and we can merge for scan planning.

But, this PR allows `And` expressions to be deserialized from JSON
through Pydantic.

This PR aligns the `And` expression with the `Or`/`Not` pattern by
adding `IcebergBaseModel` as an inherited class. This gets teh And
expression into a proven serializable state, preparing it for the full
expression tree [de]serializability work in #2783.

## Are these changes tested?

Yes added a test and ensure that they align with EpressionParser in
Iceberg Java

## Are there any user-facing changes?

No this is just serialization

cc: @kevinjqliu @Fokko

---------

Co-authored-by: Aniket Singh Yadav <singhyadavaniket43@gmail.com>
diff --git a/pyiceberg/expressions/__init__.py b/pyiceberg/expressions/__init__.py
index a928b98..71ee7cd 100644
--- a/pyiceberg/expressions/__init__.py
+++ b/pyiceberg/expressions/__init__.py
@@ -237,12 +237,19 @@
         return BoundReference
 
 
-class And(BooleanExpression):
+class And(IcebergBaseModel, BooleanExpression):
     """AND operation expression - logical conjunction."""
 
+    model_config = ConfigDict(arbitrary_types_allowed=True)
+
+    type: TypingLiteral["and"] = Field(default="and", alias="type")
     left: BooleanExpression
     right: BooleanExpression
 
+    def __init__(self, left: BooleanExpression, right: BooleanExpression, *rest: BooleanExpression) -> None:
+        if isinstance(self, And) and not hasattr(self, "left") and not hasattr(self, "right"):
+            super().__init__(left=left, right=right)
+
     def __new__(cls, left: BooleanExpression, right: BooleanExpression, *rest: BooleanExpression) -> BooleanExpression:  # type: ignore
         if rest:
             return _build_balanced_tree(And, (left, right, *rest))
@@ -254,8 +261,6 @@
             return left
         else:
             obj = super().__new__(cls)
-            obj.left = left
-            obj.right = right
             return obj
 
     def __eq__(self, other: Any) -> bool:
diff --git a/tests/expressions/test_expressions.py b/tests/expressions/test_expressions.py
index f0d6cdb..252da47 100644
--- a/tests/expressions/test_expressions.py
+++ b/tests/expressions/test_expressions.py
@@ -725,6 +725,15 @@
         null & "abc"
 
 
+def test_and_serialization() -> None:
+    expr = And(EqualTo("x", 1), GreaterThan("y", 2))
+
+    assert (
+        expr.model_dump_json()
+        == '{"type":"and","left":{"term":"x","type":"eq","value":1},"right":{"term":"y","type":"gt","value":2}}'
+    )
+
+
 def test_or() -> None:
     null = IsNull(Reference("a"))
     nan = IsNaN(Reference("b"))