diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..dbc1678bbca0a7dfeebf7ed86527ad34afa6f083
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,34 @@
+[build-system]
+requires = ["setuptools>=45", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "pyfed"
+version = "0.1.0"
+description = "ActivityPub Federation Library"
+requires-python = ">=3.8"
+dependencies = [
+    "aiohttp>=3.8.0",
+    "pydantic>=2.0.0",
+    "sqlalchemy>=2.0.0",
+    "cryptography>=3.4.0",
+    "pytest>=7.0.0",
+    "pytest-asyncio>=0.18.0",
+    "aiosqlite>=0.17.0",
+]
+
+[tool.pytest.ini_options]
+asyncio_mode = "auto"
+testpaths = ["tests"]
+python_files = ["test_*.py"]
+addopts = "-ra -q"
+markers = [
+    "asyncio: mark test as async",
+]
+
+[project.optional-dependencies]
+test = [
+    "pytest>=7.0.0",
+    "pytest-asyncio>=0.18.0",
+    "pytest-cov>=3.0.0",
+]
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..52ccba8ad30b97da82f05abb530f02f2cea6c6ed
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,32 @@
+aiohappyeyeballs==2.4.3
+aiohttp==3.10.10
+aioredis==2.0.1
+aiosignal==1.3.1
+annotated-types==0.7.0
+async-timeout==5.0.0
+attrs==24.2.0
+cffi==1.17.1
+cryptography==43.0.3
+factory_boy==3.3.1
+Faker==30.8.0
+frozenlist==1.4.1
+greenlet==3.1.1
+idna==3.10
+iniconfig==2.0.0
+multidict==6.1.0
+packaging==24.1
+pluggy==1.5.0
+prometheus_client==0.21.0
+propcache==0.2.0
+pycparser==2.22
+pydantic==2.9.2
+pydantic_core==2.23.4
+PyJWT==2.9.0
+pytest==8.3.3
+pytest-asyncio==0.24.0
+python-dateutil==2.9.0.post0
+redis==5.2.0
+six==1.16.0
+SQLAlchemy==2.0.36
+typing_extensions==4.12.2
+yarl==1.15.2
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..ca99640739d50ee7f5a4aed9f3282945f107bf88
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,7 @@
+from setuptools import setup, find_packages
+
+setup(
+    name="pyfed",
+    packages=find_packages(where="src"),
+    package_dir={"": "src"},
+) 
\ No newline at end of file
diff --git a/src/pyfed/README.md b/src/pyfed/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..707ef17c04fff8e45da98cca814c6f7b8564c01d
--- /dev/null
+++ b/src/pyfed/README.md
@@ -0,0 +1,269 @@
+# Building an ActivityPub Library for Python Developers
+## Introduction
+ActivityPub is a protocol for social networking that is decentralized enabling the interchange of data between different social media systems. Thus, its aim of this library is to create the ActivityPub protocol in the form of Python language to the benefit of python programmers who wish to incorporate ActivityPub in their Apps. With that in mind, it is clear that ActivityPub would transform our online interactions for the better as it encourages users to leave the centralized, client-server model for more decentralized, federated networks. This library will appeal to developer of decentralized social networks, enthusiastic studying the federated protocols and all those who would like to develop social applications.
+
+## Approach
+To build a robust and reliable ActivityPub library, we will follow these steps:
+
+1. **Model Definition**
+- Define Python classes for ActivityPub entities (e.g., Actor, Object, Activity) using Pydantic for data validation and serialization.
+
+**Example Model Definition**
+
+```python
+from pydantic import BaseModel, HttpUrl
+   from typing import List, Optional
+
+   class Actor(BaseModel):
+       id: HttpUrl
+       type: str
+       name: str
+       inbox: HttpUrl
+       outbox: HttpUrl
+
+   class Object(BaseModel):
+       id: HttpUrl
+       type: str
+       attributedTo: Actor
+       to: List[Actor] = []
+       content: str
+
+   class Activity(BaseModel):
+       id: HttpUrl
+       type: str
+       actor: Actor
+       object: Object
+       to: List[Actor] = []
+       context: Optional[str] = "https://www.w3.org/ns/activitystreams"
+
+   actor = Actor(
+       id="https://chatty.example/ben/",
+       type="Person",
+       name="Ben",
+       inbox="https://chatty.example/ben/inbox",
+       outbox="https://chatty.example/ben/outbox"
+   )
+   obj = Object(
+       id="https://example.com/message",
+       type="Note",
+       attributedTo=actor,
+       to=[actor],
+       content="Hello, World!"
+   )
+...
+```
+
+2. **Serialization and Deserialization**
+- Implement methods to convert Pydantic models to and from JSON-LD format, which is used by ActivityStreams.
+- Pydantic provides built-in methods for serialization and deserialization, simplifying the process.
+
+```python
+    from pydantic import BaseModel
+    from typing import Any
+
+    class JsonSerializer:
+        def serialize(self, obj: BaseModel) -> str:
+            return obj.json(indent=4)
+        
+        def deserialize(self, json_str: str, model: BaseModel) -> BaseModel:
+            return model.parse_raw(json_str)
+
+    serializer = JsonSerializer()
+
+    # Serialize an activity
+    serialized_activity = serializer.serialize(activity)
+
+    # Deserialize back to an Activity object
+    deserialized_activity = serializer.deserialize(serialized_activity, Activity)
+```
+
+3. **Server Endpoints**
+- Design a client class for sending ActivityPub requests (e.g., `ActivityPubClient`).
+- Implement server endpoints for receiving ActivityPub requests (e.g., `ActivityPubServer`).
+- Use a library like `aiohttp` for HTTP client functionality.
+**Example Server Endpoint**
+
+```python
+from aiohttp import web
+
+   class ActivityPubServer:
+       async def handle_inbox_request(self, request: web.Request) -> web.Response:
+           # This method will parse the incoming JSON-LD,
+           # validate the sender's signature, and route the activity to the appropriate handler.
+           data = await request.json()
+           activity = Activity.parse_obj(data)  # Validate and parse incoming data
+           # Process the incoming activity...
+           ...
+
+       async def handle_outbox_request(self, request: web.Request) -> web.Response:
+           # This method will parse the outgoing activity and prepare it for delivery.
+           data = await request.json()
+           activity = Activity.parse_obj(data)  # Validate and parse incoming data
+           # Prepare and send the outgoing activity...
+        ...
+
+app = web.Application()
+app.add_routes([web.post("/inbox", ActivityPubServer().handle_inbox_request)])
+app.add_routes([web.post("/outbox", ActivityPubServer().handle_outbox_request)])
+web.run_app(app)
+
+```
+
+4. **Activity Handling**
+    - Implement activity handling logic, including:
+        - Activity creation and validation.
+        - Activity forwarding and delivery.
+        - Activity processing and updating.
+
+    **Example Activity Handling**
+
+    ```python
+    class ActivityHandler:
+        def create_activity(self, actor: Actor, object: Object) -> Activity:
+            # Logic to create an activity
+            return Activity(id="some_id", type="Create", actor=actor, object=object, to=[actor])
+
+        def forward_activity(self, activity: Activity) -> None:
+            # Logic to forward an activity
+            ...
+
+        def process_activity(self, activity: Activity) -> None:
+            # Logic to process an activity
+            ...
+
+
+    class CreateActivityHandler(ActivityHandler):
+        def process_activity(self, activity: Activity) -> None:
+            # Logic specific to processing create activities
+            ...
+
+    class FollowActivityHandler(ActivityHandler):
+        def process_activity(self, activity: Activity) -> None:
+            # Logic specific to processing follow activities
+            ...
+
+    class LikeActivityHandler(ActivityHandler):
+        def process_activity(self, activity: Activity) -> None:
+            # Logic specific to processing like activities
+            ...
+    # Handlers would be selected dynamically based on activity type.
+    ```
+
+**Example Use Cases**
+- Sending a message from one actor to another
+- Creating a new activity and forwarding it to other actors
+- Processing incoming activities and updating the local state
+
+**Library Structure**
+The library will be structured as follows:
+
+```
+pyfed/
+│
+├── __init__.py
+│
+├── models/             # Contains data models for ActivityPub entities
+│   ├── __init__.py
+│   ├── actor.py
+│   ├── object.py
+│   └── activity.py
+│
+├── serializers/        # Handles serialization and deserialization of models
+│   ├── __init__.py
+│   ├── json_serializer.py
+│   ├── client.py
+│   └── server.py
+│
+├── utils.py            # Common utility functions and validation helpers
+│
+├── security/           # Implement security features like HTTP signatures
+│   ├── __init__.py
+│   └── http_signatures.py
+│
+├── tests/              # Test cases for models, serializers, client, and server
+│   ├── __init__.py
+│   ├── test_models.py
+│   ├── test_serializers.py
+│   ├── test_client.py
+│   └── test_server.py
+│
+├── CONTRIBUTING.md
+└── README.md
+```
+
+## Installation and Usage Instructions
+To install the library, run 
+
+```bash
+pip install pyfed
+```
+
+**Basic Usage Example**
+Here’s a simple example of how to instantiate the server, create an actor and an object, and invoke an endpoint:
+
+```python
+from activitypub import ActivityPubServer, Actor, Object
+
+# Initialize server
+server = ActivityPubServer()
+
+# Create an actor and an object
+actor = Actor(
+    id="https://chatty.example/ben/",
+    type="Person",
+    name="Ben",
+    inbox="https://chatty.example/ben/inbox",
+    outbox="https://chatty.example/ben/outbox"
+)
+obj = Object(
+    id="https://example.com/message",
+    type="Note",
+    attributedTo=actor,
+    to=[actor],
+    content="Hello, World!"
+)
+
+# Create and serialize an activity
+activity = server.create_activity(actor, obj)
+serialized_activity = server.serialize(activity)
+
+# (In a real scenario, you would send this serialized activity via a request)
+```
+
+## Contribution Guidelines
+We welcome contributions to the library. Please see the `CONTRIBUTING.md` file for guidelines on how to contribute. You can report issues or request features on the [GitHub issues page](https://github.com/yourusername/pyfed/issues).
+
+## Dependencies
+The library depends on the following major dependencies:
+- `aiohttp` (version >=3.7.4) for HTTP client functionality.
+- `pydantic` (version >=1.8.0) for data validation and serialization.
+- `pytest` (version >=6.0) for testing.
+
+## Future Plans
+We plan to implement additional features, including:
+- Support for more ActivityPub endpoints (e.g., followers, following).
+- Improved security and authentication mechanisms.
+- Enhanced activity handling logic.
+- Better error handling and debugging tools.
+
+For a detailed roadmap of the project, see the `ROADMAP.md` file.
+
+## Additional Resources
+For more information on ActivityPub and its ecosystem, please see the following resources:
+
+- [ActivityPub specification](https://www.w3.org/TR/activitypub/)
+- [ActivityStreams standard](https://www.w3.org/TR/activitystreams-core/)
+- [JSON-LD specification](https://www.w3.org/TR/json-ld/)
+
+## License
+This project is licensed under the MIT License. See the `LICENSE` file for details.
+
+## Timeline
+**Review of Library by October**
+**Completion by December**
+
+---
+
+This is just a starting point, and the approach may evolve as we progress with the implementation. We welcome contributions and feedback from the community to help shape the library into a robust and reliable implementation of the ActivityPub protocol.
+
+![ActivityPub](flow.drawio.png)
\ No newline at end of file
diff --git a/src/pyfed/models/__init__.py b/src/pyfed/models/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..365021f853072f44e6df0cba3f20b54c06d716e4
--- /dev/null
+++ b/src/pyfed/models/__init__.py
@@ -0,0 +1,27 @@
+from typing import Union
+
+# from .base import APBase, APObject, APLink, APActivity
+from .actors import (
+    APActor, APPerson, APGroup, APOrganization, APApplication, APService
+)
+from .objects import (
+    APEvent, APPlace, APProfile, APRelationship, APTombstone,
+    APArticle, APAudio, APDocument, APImage, APNote, APPage, APVideo, APObject
+)
+from .collections import (
+    APCollection, APOrderedCollection, APCollectionPage, APOrderedCollectionPage
+)
+from .activities import (
+    APCreate, APUpdate, APDelete,
+    APFollow, APUndo, APLike, APAnnounce, APActivity
+)
+from .links import APLink, APMention
+
+# Export all classes
+__all__ = [
+    'APBase', 'APObject', 'APLink', 'APActivity',
+    'APActor', 'APPerson', 'APGroup', 'APOrganization', 'APApplication', 'APService',
+    'APEvent', 'APPlace', 'APProfile', 'APRelationship', 'APTombstone', 'APArticle', 'APAudio', 'APDocument', 'APImage', 'APNote', 'APPage', 'APVideo',
+    'APCollection', 'APOrderedCollection', 'APCollectionPage', 'APOrderedCollectionPage',
+    'APCreate', 'APUpdate', 'APDelete', 'APFollow', 'APUndo', 'APLike', 'APAnnounce', 'APMention'
+]
diff --git a/src/pyfed/models/activities.py b/src/pyfed/models/activities.py
new file mode 100644
index 0000000000000000000000000000000000000000..50646e21d31c1f7381f51e747b8c92e8ce2a9d26
--- /dev/null
+++ b/src/pyfed/models/activities.py
@@ -0,0 +1,183 @@
+"""
+activities.py
+This module defines the Activity types for ActivityPub.
+These activities are based on the ActivityStreams vocabulary.
+
+For more information, see:
+https://www.w3.org/TR/activitystreams-vocabulary/#activity-types
+"""
+
+from pydantic import Field, HttpUrl
+from typing import Optional, Union, Literal, Dict, Any
+from pyfed.models.objects import APObject
+
+class APActivity(APObject):
+    """
+    Base class for all ActivityPub activities.
+    
+    An Activity is a subtype of Object that describes some form of action.
+
+    Properties:
+        actor (Union[str, HttpUrl, APObject]): The actor performing the activity.
+        object (Optional[Union[str, HttpUrl, APObject, Dict[str, Any]]]): The object of the activity.
+        target (Optional[Union[str, HttpUrl, APObject]]): The target of the activity.
+        result (Optional[Union[str, HttpUrl, APObject, Dict[str, Any]]]): The result of the activity.
+        origin (Optional[Union[str, HttpUrl, APObject]]): The origin of the activity.
+        instrument (Optional[Union[str, HttpUrl, APObject]]): The instrument used to perform the activity.
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#activity
+    Usage: https://www.w3.org/TR/activitypub/#activities
+    """
+    actor: Union[str, HttpUrl, APObject] = Field(..., description="The actor performing the activity")
+    object: Optional[Union[str, HttpUrl, APObject, Dict[str, Any]]] = Field(
+        default=None,
+        description="The object of the activity"
+    )
+    target: Optional[Union[str, HttpUrl, APObject]] = Field(default=None, description="The target of the activity")
+    result: Optional[Union[str, HttpUrl, APObject, Dict[str, Any]]] = Field(
+        default=None,
+        description="The result of the activity"
+    )
+    origin: Optional[Union[str, HttpUrl, APObject]] = Field(default=None, description="The origin of the activity")
+    instrument: Optional[Union[str, HttpUrl, APObject]] = Field(default=None, description="The instrument used")
+
+class APIntransitiveActivity(APActivity):
+    """
+    Represents an IntransitiveActivity in ActivityPub.
+    
+    An IntransitiveActivity is an Activity that does not have an object.
+
+    Properties:
+        type (Literal["IntransitiveActivity"]): Always set to "IntransitiveActivity".
+        object (None): Always set to None for IntransitiveActivity.
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#intransitiveactivity
+    """
+    type: Literal["IntransitiveActivity"] = Field(default="IntransitiveActivity")
+    object: None = None
+
+class APCreate(APActivity):
+    """
+    Represents a Create activity in ActivityPub.
+
+    Indicates that the actor has created the object.
+
+    Attributes:
+        type (Literal["Create"]): Always set to "Create".
+        object ([Union[str, HttpUrl, APObject, Dict[str, Any]]]): The object of the activity.
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#create
+    """
+    type: Literal["Create"] = Field(default="Create", description="Indicates that this object represents a create activity")
+    object: Union[str, HttpUrl, APObject, Dict[str, Any]] = Field(
+        ...,  
+        description="The object being created"
+    )
+
+class APUpdate(APActivity):
+    """
+    Represents an Update activity in ActivityPub.
+
+    Indicates that the actor has updated the object.
+
+    Attributes:
+        type (Literal["Update"]): Always set to "Update".
+        object ([Union[str, HttpUrl, APObject, Dict[str, Any]]]): The object of the activity.
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#update
+    """
+    type: Literal["Update"] = Field(default="Update", description="Indicates that this object represents an update activity")
+    object: Union[str, HttpUrl, APObject, Dict[str, Any]] = Field(
+        ...,  
+        description="The object being updated"
+    )
+
+class APDelete(APActivity):
+    """
+    Represents a Delete activity in ActivityPub.
+
+    Indicates that the actor has deleted the object.
+
+    Attributes:
+        type (Literal["Delete"]): Always set to "Delete".
+        object ([Union[str, HttpUrl, APObject, Dict[str, Any]]]): The object of the activity.
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#delete
+    """
+    type: Literal["Delete"] = Field(default="Delete", description="Indicates that this object represents a delete activity")
+    object: Union[str, HttpUrl, APObject, Dict[str, Any]] = Field(
+        ...,  
+        description="The object being deleted"
+    )
+
+class APFollow(APActivity):
+    """
+    Represents a Follow activity in ActivityPub.
+
+    Indicates that the actor is "following" the object.
+
+    Attributes:
+        type (Literal["Follow"]): Always set to "Follow".
+        object ([Union[str, HttpUrl, APObject, Dict[str, Any]]]): The object of the activity.
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#follow
+    """
+    type: Literal["Follow"] = Field(default="Follow", description="Indicates that this object represents a follow activity")
+    object: Union[str, HttpUrl, APObject] = Field(
+        ...,  
+        description="The object being followed"
+    )
+
+class APUndo(APActivity):
+    """
+    Represents an Undo activity in ActivityPub.
+
+    Indicates that the actor is undoing the object activity.
+
+    Attributes:
+        type (Literal["Undo"]): Always set to "Undo".
+        object ([Union[str, HttpUrl, APObject, Dict[str, Any]]]): The object of the activity.
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#undo
+    """
+    type: Literal["Undo"] = Field(default="Undo", description="Indicates that this object represents an undo activity")
+    object: Union[str, HttpUrl, APObject, Dict[str, Any]] = Field(
+        ...,  
+        description="The activity being undone"
+    )
+
+class APLike(APActivity):
+    """
+    Represents a Like activity in ActivityPub.
+
+    Indicates that the actor likes the object.
+
+    Attributes:
+        type (Literal["Like"]): Always set to "Like".
+        object ([Union[str, HttpUrl, APObject, Dict[str, Any]]]): The object of the activity.
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#like
+    """
+    type: Literal["Like"] = Field(default="Like", description="Indicates that this object represents a like activity")
+    object: Union[str, HttpUrl, APObject] = Field(
+        ...,  
+        description="The object being liked"
+    )
+
+class APAnnounce(APActivity):
+    """
+    Represents an Announce activity in ActivityPub.
+
+    Indicates that the actor is calling the target's attention to the object.
+
+    Attributes:
+        type (Literal["Announce"]): Always set to "Announce".
+        object ([Union[str, HttpUrl, APObject, Dict[str, Any]]]): The object of the activity.
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#announce
+    """
+    type: Literal["Announce"] = Field(default="Announce", description="Indicates that this object represents an announce activity")
+    object: Union[str, HttpUrl, APObject] = Field(
+        ...,  
+        description="The object being announced"
+    )
diff --git a/src/pyfed/models/actors.py b/src/pyfed/models/actors.py
new file mode 100644
index 0000000000000000000000000000000000000000..0a5fc0b1cbc777d4546e64271e059a6331350ea7
--- /dev/null
+++ b/src/pyfed/models/actors.py
@@ -0,0 +1,200 @@
+"""
+actors.py
+This module defines the Actor types for ActivityPub.
+These actors are based on the ActivityStreams vocabulary and ActivityPub protocol.
+
+For more information on Actors in ActivityPub, see:
+https://www.w3.org/TR/activitypub/#actors
+"""
+
+from __future__ import annotations
+from pydantic import Field, HttpUrl, field_validator
+from typing import Optional, List, Dict, Any, TypedDict, Literal
+from datetime import datetime
+from pyfed.utils.exceptions import InvalidURLError
+# from pyfed.plugins import plugin_manager
+from pyfed.utils.logging import get_logger
+from pyfed.cache import object_cache
+
+from .objects import APObject
+
+logger = get_logger(__name__)
+
+class ActivityDict(TypedDict):
+    actor: str
+    object: Dict[str, Any]
+    published: str
+    type: str
+    context: str
+
+class APActor(APObject):
+    """
+    Base Actor type as defined in ActivityPub.
+
+    Properties:
+        type (Literal["Person"]): Always set to "Person".
+        inbox (HttpUrl): The actor's inbox URL.
+        outbox (HttpUrl): The actor's outbox URL.
+        following (Optional[HttpUrl]): The actor's following collection URL.
+        followers (Optional[HttpUrl]): The actor's followers collection URL.
+        liked (Optional[HttpUrl]): The actor's liked collection URL.
+        streams (Optional[List[HttpUrl]]): Additional streams.
+        preferred_username (Optional[str]): The actor's preferred username.
+        endpoints (Optional[Dict[str, HttpUrl]]): Additional endpoints.
+        public_key (Optional[Dict[str, str]]): Public key information.
+
+    Specification: https://www.w3.org/TR/activitypub/#actor-objects
+    Usage: https://www.w3.org/TR/activitypub/#actor-objects
+    """
+    inbox: HttpUrl = Field(..., description="Primary inbox URL")
+    outbox: HttpUrl = Field(..., description="Primary outbox URL")
+    following: Optional[HttpUrl] = Field(default=None, description="Following collection")
+    followers: Optional[HttpUrl] = Field(default=None, description="Followers collection")
+    liked: Optional[HttpUrl] = Field(default=None, description="Liked collection")
+    streams: Optional[List[HttpUrl]] = Field(default=None, description="Additional streams")
+    preferred_username: Optional[str] = Field(default=None, description="Preferred username")
+    endpoints: Optional[Dict[str, HttpUrl]] = Field(default=None, description="Additional endpoints")
+    public_key: Optional[Dict[str, str]] = Field(default=None, description="Public key information")
+
+    @field_validator('inbox', 'outbox', 'following', 'followers', 'liked')
+    @classmethod
+    def validate_urls(cls, v):
+        try:
+            return HttpUrl(v)
+        except ValueError:
+            raise InvalidURLError(f"Invalid URL: {v}")
+
+    async def send_to_inbox(self, activity: Dict[str, Any]) -> bool:
+        """
+        Send an activity to this actor's inbox.
+
+        Args:
+            activity (Dict[str, Any]): The activity to send.
+
+        Returns:
+            bool: True if the activity was successfully delivered, False otherwise.
+
+        Note: This method should implement the logic described in:
+        https://www.w3.org/TR/activitypub/#delivery
+        """
+        logger.info(f"Sending activity to inbox: {self.inbox}")
+        # Execute pre-send hook
+        # plugin_manager.execute_hook('pre_send_to_inbox', self, activity)
+        
+        # Placeholder for actual implementation
+        return True
+
+    async def fetch_followers(self) -> List[APActor]:
+        """
+        Fetch the followers of this actor.
+
+        Returns:
+            List[APActor]: A list of actors following this actor.
+        """
+        logger.info(f"Fetching followers for actor: {self.id}")
+        
+        # Check cache first
+        cached_followers = object_cache.get(f"followers:{self.id}")
+        if cached_followers is not None:
+            return cached_followers
+
+        # Execute pre-fetch hook
+        # plugin_manager.execute_hook('pre_fetch_followers', self)
+        
+        # Fetch followers (placeholder implementation)
+        followers = []  # Actual implementation would go here
+        
+        # Cache the result
+        object_cache.set(f"followers:{self.id}", followers)
+        
+        return followers
+
+    async def create_activity(self, activity_type: str, object: Dict[str, Any]) -> ActivityDict:
+        """
+        Create an activity with this actor as the 'actor'.
+
+        Args:
+            activity_type (str): The type of activity (e.g., "Create", "Like", "Follow").
+            object (Dict[str, Any]): The object of the activity.
+
+        Returns:
+            Dict[str, Any]: The created activity.
+
+        Note: This method should create activities as described in:
+        Specification: https://www.w3.org/TR/activitypub/#create-activity-outbox
+        """
+        logger.debug(f"Creating activity of type: {activity_type}")
+        return {
+            "@context": "https://www.w3.org/ns/activitystreams",
+            "type": activity_type,
+            "actor": self.id,
+            "object": object,
+            "published": datetime.now(datetime.UTC).isoformat() + "Z"
+        }
+
+class APPerson(APActor):
+    """
+    Represents a Person actor in ActivityPub.
+
+    A Person is an individual user account.
+
+    Properties:
+        type (Literal["Person"]): Always set to "Person".
+
+    Inherits all other properties from APActor.
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person
+    """
+    type: Literal["Person"] = Field(default="Person", description="Indicates that this object represents a person")
+
+class APGroup(APActor):
+    """
+    Represents a Group actor in ActivityPub.
+
+    A Group represents a formal or informal collective of Actors.
+
+    Attributes:
+        type (str): Always set to "Group".
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-group
+    """
+    type: Literal["Group"] = Field(default="Group", description="Indicates that this object represents a group")
+
+class APOrganization(APActor):
+    """
+    Represents an Organization actor in ActivityPub.
+
+    An Organization is a kind of Actor representing a formal or informal organization.
+
+    Attributes:
+        type (str): Always set to "Organization".
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-organization
+    """
+    type: Literal["Organization"] = Field(default="Organization", description="Indicates that this object represents an organization")
+
+class APApplication(APActor):
+    """
+    Represents an Application actor in ActivityPub.
+
+    An Application represents a software application.
+
+    Attributes:
+        type (str): Always set to "Application".
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-application
+    """
+    type: Literal["Application"] = Field(default="Application", description="Indicates that this object represents an application")
+
+class APService(APActor):
+    """
+    Represents a Service actor in ActivityPub.
+
+    A Service is a kind of Actor that represents a service of any kind.
+
+    Attributes:
+        type (str): Always set to "Service".
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-service
+    """
+    type: Literal["Service"] = Field(default="Service", description="Indicates that this object represents a service")
diff --git a/src/pyfed/models/collections.py b/src/pyfed/models/collections.py
new file mode 100644
index 0000000000000000000000000000000000000000..64a9759b43864b3e8f0eb802a28ab3ca1a335a84
--- /dev/null
+++ b/src/pyfed/models/collections.py
@@ -0,0 +1,140 @@
+"""
+collections.py
+This module defines the Collection types for ActivityPub.
+These collections are based on the ActivityStreams vocabulary and ActivityPub protocol.
+
+For more information on Collections in ActivityStreams, see:
+https://www.w3.org/TR/activitystreams-core/#collections
+"""
+
+from __future__ import annotations
+from typing import Optional, List, Union, Literal, Dict, Any
+from pydantic import Field, HttpUrl
+from pyfed.models.objects import APObject
+from pyfed.utils.logging import get_logger
+
+logger = get_logger(__name__)
+
+class APCollection(APObject):
+    """
+    Represents a Collection in ActivityPub.
+    
+    A Collection is an unordered set of Objects or Links.
+
+    Properties:
+        type (Literal["Collection"]): Always set to "Collection".
+        total_items (Optional[int]): The total number of items in the collection.
+        current (Optional[Union[str, HttpUrl, 'APCollectionPage']]): The current page.
+        first (Optional[Union[str, HttpUrl, 'APCollectionPage']]): The first page.
+        last (Optional[Union[str, HttpUrl, 'APCollectionPage']]): The last page.
+        items (Optional[List[Union[str, HttpUrl, Dict[str, Any], APObject]]]): The items.
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#collection
+    Usage: https://www.w3.org/TR/activitypub/#collections
+    """
+    type: Literal["Collection"] = Field(default="Collection", description="Indicates a Collection")
+    total_items: Optional[int] = Field(
+        default=None,
+        ge=0,  # Greater than or equal to 0
+        description="Total number of items"
+    )
+    current: Optional[Union[str, HttpUrl, 'APCollectionPage']] = Field(default=None, description="Current page")
+    first: Optional[Union[str, HttpUrl, 'APCollectionPage']] = Field(default=None, description="First page")
+    last: Optional[Union[str, HttpUrl, 'APCollectionPage']] = Field(default=None, description="Last page")
+    items: Optional[List[Union[str, HttpUrl, Dict[str, Any], APObject]]] = Field(default=None, description="Items in the collection")
+
+class APOrderedCollection(APCollection):
+    """
+    Represents an OrderedCollection in ActivityPub.
+    
+    An OrderedCollection is a Collection where the items are strictly ordered.
+
+    Properties:
+        type (Literal["OrderedCollection"]): Always set to "OrderedCollection".
+        ordered_items (Optional[List[Union[str, HttpUrl, Dict[str, Any], APObject]]]): The ordered items.
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#orderedcollection
+    Usage: https://www.w3.org/TR/activitypub/#ordered-collections
+    """
+    type: Literal["OrderedCollection"] = Field(default="OrderedCollection", description="Indicates an OrderedCollection")
+    ordered_items: Optional[List[Union[str, HttpUrl, Dict[str, Any], APObject]]] = Field(default=None, description="The ordered items")
+
+class APCollectionPage(APCollection):
+    """
+    Represents a CollectionPage in ActivityPub.
+    
+    A CollectionPage represents a single page of a Collection.
+
+    Properties:
+        type (Literal["CollectionPage"]): Always set to "CollectionPage".
+        part_of (Optional[Union[str, HttpUrl, APCollection]]): The parent collection.
+        next (Optional[Union[str, HttpUrl, 'APCollectionPage']]): The next page.
+        prev (Optional[Union[str, HttpUrl, 'APCollectionPage']]): The previous page.
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#collectionpage
+    Usage: https://www.w3.org/TR/activitypub/#collection-paging
+    """
+    type: Literal["CollectionPage"] = Field(default="CollectionPage", description="Indicates a CollectionPage")
+    part_of: Optional[Union[str, HttpUrl, APCollection]] = Field(default=None, description="The parent collection")
+    next: Optional[Union[str, HttpUrl, 'APCollectionPage']] = Field(default=None, description="The next page")
+    prev: Optional[Union[str, HttpUrl, 'APCollectionPage']] = Field(default=None, description="The previous page")
+
+class APOrderedCollectionPage(APCollectionPage):
+    """
+    Represents an OrderedCollectionPage in ActivityPub.
+    
+    An OrderedCollectionPage represents a single page of an OrderedCollection.
+
+    Properties:
+        type (Literal["OrderedCollectionPage"]): Always set to "OrderedCollectionPage".
+        ordered_items (Optional[List[Union[str, HttpUrl, Dict[str, Any], APObject]]]): The ordered items.
+        start_index (Optional[int]): The index of the first item (0-based).
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#orderedcollectionpage
+    Usage: https://www.w3.org/TR/activitypub/#ordered-collection-paging
+    """
+    type: Literal["OrderedCollectionPage"] = Field(default="OrderedCollectionPage", description="Indicates an OrderedCollectionPage")
+    ordered_items: Optional[List[Union[str, HttpUrl, Dict[str, Any], APObject]]] = Field(default=None, description="The ordered items")
+    start_index: Optional[int] = Field(
+        default=None,
+        ge=0,  # Greater than or equal to 0
+        description="The index of the first item"
+    )
+
+async def fetch_collection(url: str) -> Union[APCollection, APOrderedCollection]:
+    """
+    Fetch a collection from a given URL.
+
+    Args:
+        url (str): The URL of the collection to fetch.
+
+    Returns:
+        Union[APCollection, APOrderedCollection]: The fetched collection.
+
+    Note: This is a placeholder implementation. In a real-world scenario,
+    this would involve making an HTTP request to the given URL and
+    parsing the response into the appropriate collection type.
+    """
+    logger.info(f"Fetching collection from: {url}")
+    # Placeholder implementation
+    # print(f"Fetching collection from: {url}")
+    return APCollection(id=url, type="Collection", totalItems=0)
+
+async def paginate_collection(collection: Union[APCollection, APOrderedCollection], page_size: int = 10) -> List[Union[APCollectionPage, APOrderedCollectionPage]]:
+    """
+    Paginate a collection into smaller pages.
+
+    Args:
+        collection (Union[APCollection, APOrderedCollection]): The collection to paginate.
+        page_size (int): The number of items per page.
+
+    Returns:
+        List[Union[APCollectionPage, APOrderedCollectionPage]]: A list of collection pages.
+
+    Note: This is a placeholder implementation. In a real-world scenario,
+    this would involve creating actual page objects and properly linking them.
+    """
+    logger.info(f"Paginating collection: {collection.id}")
+    # Placeholder implementation
+    # print(f"Paginating collection: {collection.id}")
+    return []
diff --git a/src/pyfed/models/links.py b/src/pyfed/models/links.py
new file mode 100644
index 0000000000000000000000000000000000000000..fcb7dde57060bda04b67eb142db982419aec6349
--- /dev/null
+++ b/src/pyfed/models/links.py
@@ -0,0 +1,69 @@
+"""
+links.py
+This module defines the Link types for ActivityPub.
+These links are based on the ActivityStreams vocabulary.
+
+For more information, see:
+https://www.w3.org/TR/activitystreams-vocabulary/#link
+"""
+
+from pydantic import Field, HttpUrl, field_validator
+from typing import Optional, List, Literal
+from pyfed.serializers.json_serializer import ActivityPubBase
+
+class APLink(ActivityPubBase):
+    """
+    Represents a Link in ActivityPub.
+    
+    A Link is an indirect, qualified reference to a resource identified by a URL.
+    It can be used to represent a variety of connections between objects in the ActivityPub ecosystem.
+
+    Properties:
+        type (Literal["Link"]): The type of the object, always "Link" for this class.
+        href (HttpUrl): The target resource pointed to by the link.
+        rel (Optional[List[str]]): The relationship between the resource and the link.
+        mediaType (Optional[str]): The MIME media type of the referenced resource.
+        name (Optional[str]): A human-readable name for the link.
+        hreflang (Optional[str]): The language of the referenced resource.
+        height (Optional[int]): The height of the referenced resource in pixels.
+        width (Optional[int]): The width of the referenced resource in pixels.
+        preview (Optional[HttpUrl]): A link to a preview of the referenced resource.
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#link
+    Usage: https://www.w3.org/TR/activitypub/#object-without-create
+    """
+    type: Literal["Link"] = Field(default="Link", description="Indicates that this object represents a link")
+    href: HttpUrl = Field(..., description="The target resource pointed to by the link")
+    rel: Optional[List[str]] = Field(default=None, description="The relationship between the resource and the link")
+    media_type: Optional[str] = Field(default=None, description="The MIME media type of the referenced resource")
+    name: Optional[str] = Field(default=None, description="A human-readable name for the link")
+    hreflang: Optional[str] = Field(default=None, description="The language of the referenced resource")
+    height: Optional[int] = Field(default=None, description="The height of the referenced resource in pixels")
+    width: Optional[int] = Field(default=None, description="The width of the referenced resource in pixels")
+    preview: Optional[HttpUrl] = Field(default=None, description="A link to a preview of the referenced resource")
+
+    @field_validator('media_type')
+    def validate_media_type(cls, value):
+        allowed_mimes = ["image/jpeg", "image/png", "application/json", "text/html", "video/mp4"]
+        if value and not value in allowed_mimes:
+            raise ValueError(f"Invalid MIME type: {value}")
+        return value
+
+class APMention(APLink):
+    """
+    Represents a Mention in ActivityPub.
+    
+    A Mention is a specialized Link that represents an @mention.
+    It is typically used to reference another actor in the content of an object.
+
+    Properties:
+        type (Literal["Mention"]): The type of the object, always "Mention" for this class.
+        href (HttpUrl): The URL of the mentioned actor.
+        name (Optional[str]): The name or username of the mentioned actor.
+
+    Inherits all other properties from APLink.
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#mention
+    Usage: https://www.w3.org/TR/activitypub/#mention
+    """
+    type: Literal["Mention"] = Field(default="Mention", description="Indicates that this object represents a mention")
diff --git a/src/pyfed/models/objects.py b/src/pyfed/models/objects.py
new file mode 100644
index 0000000000000000000000000000000000000000..032a573d01d4c48b14957b40e5a63d28c0202a1a
--- /dev/null
+++ b/src/pyfed/models/objects.py
@@ -0,0 +1,314 @@
+"""
+objects.py
+This module defines the core object types for ActivityPub.
+These objects are based on the ActivityStreams vocabulary.
+
+For more information, see:
+https://www.w3.org/TR/activitystreams-vocabulary/
+"""
+
+from ..serializers.json_serializer import ActivityPubBase
+from pydantic import Field, HttpUrl, field_validator, model_validator
+from typing import Optional, List, Union, Literal, Dict
+from datetime import datetime
+from pyfed.models.links import APLink
+
+class APObject(ActivityPubBase):
+    """Base class for all ActivityPub objects.
+    
+    According to ActivityStreams 2.0 Core Types:
+    https://www.w3.org/TR/activitystreams-core/#object
+
+    Properties:
+        id (HttpUrl): Unique identifier for the object.
+        type (str): The type of the object.
+        attachment (Optional[List[Union[str, 'APObject']]]): Files or objects attached.
+        attributed_to (Optional[Union[str, 'APObject']]): Entity responsible for this object.
+        audience (Optional[List[Union[str, 'APObject']]]): Intended audience.
+        content (Optional[str]): The content/body of the object.
+        context (Optional[Union[str, 'APObject']]): Context of the object.
+        name (Optional[str]): Human-readable title.
+        end_time (Optional[datetime]): When the object concludes.
+        generator (Optional[Union[str, 'APObject']]): Application that generated this.
+        icon (Optional[Union[str, 'APObject']]): Small image representation.
+        image (Optional[Union[str, 'APObject']]): Larger image representation.
+        in_reply_to (Optional[Union[str, HttpUrl, 'APObject']]): Object being replied to.
+        location (Optional[Union[str, 'APObject']]): Physical or logical location.
+        preview (Optional[Union[str, 'APObject']]): Preview of this object.
+        published (Optional[datetime]): Publication timestamp.
+        replies (Optional[Union[str, 'APObject']]): Collection of replies.
+        start_time (Optional[datetime]): When the object begins.
+        summary (Optional[str]): Brief description.
+        tag (Optional[List[Union[str, Dict[str, str], 'APObject', 'APLink']]]): Associated tags.
+        updated (Optional[datetime]): Last update timestamp.
+        url (Optional[Union[HttpUrl, List[HttpUrl]]]): Link to the object.
+        to (Optional[List[Union[str, HttpUrl]]]): Primary recipients.
+        bto (Optional[List[Union[str, HttpUrl]]]): Private primary recipients.
+        cc (Optional[List[Union[str, HttpUrl]]]): Secondary recipients.
+        bcc (Optional[List[Union[str, HttpUrl]]]): Private secondary recipients.
+        media_type (Optional[str]): MIME type of the object.
+
+    Specification: https://www.w3.org/TR/activitystreams-vocabulary/#object
+    Usage: https://www.w3.org/TR/activitypub/#object
+    """
+    id: HttpUrl = Field(..., description="Unique identifier for the object")
+    type: str = Field(..., description="The type of the object")
+    attachment: Optional[List[Union[str, 'APObject']]] = Field(default=None, description="Files attached to the object")
+    attributed_to: Optional[Union[str, 'APObject']] = Field(default=None, description="Entity attributed to this object")
+    audience: Optional[List[Union[str, 'APObject']]] = Field(default=None, description="Intended audience")
+    content: Optional[str] = Field(default=None, description="The content of the object")
+    context: Optional[Union[str, 'APObject']] = Field(default=None, description="Context of the object")
+    name: Optional[str] = Field(default=None, description="The name/title of the object")
+    end_time: Optional[datetime] = Field(default=None, description="End time of the object")
+    generator: Optional[Union[str, 'APObject']] = Field(default=None, description="Application that generated the object")
+    icon: Optional[Union[str, 'APObject']] = Field(default=None, description="Icon representing the object")
+    image: Optional[Union[str, 'APObject']] = Field(default=None, description="Image representing the object")
+    in_reply_to: Optional[Union[str, HttpUrl, 'APObject']] = Field(
+        default=None, 
+        description="Object this is in reply to"
+    )
+    location: Optional[Union[str, 'APObject']]= Field(default=None, description="Location associated with the object")
+    preview: Optional[Union[str, 'APObject']]= Field(default=None, description="Preview of the object")
+    published: Optional[datetime] = Field(default=None, description="Publication date")
+    replies: Optional[Union[str, 'APObject']]= Field(default=None, description="Replies to this object")
+    start_time: Optional[datetime] = Field(default=None, description="Start time of the object")
+    summary: Optional[str] = Field(default=None, description="Summary of the object")
+    tag: Optional[List[Union[str, Dict[str, str], 'APObject', 'APLink']]] = Field(
+        default=None, 
+        description="Tags associated with the object"
+    )
+    updated: Optional[datetime] = Field(default=None, description="Last update time")
+    url: Optional[Union[HttpUrl, List[HttpUrl]]] = Field(default=None, description="URL of the object")
+    to: Optional[List[Union[str, HttpUrl]]] = Field(default=None, description="Primary recipients")
+    bto: Optional[List[Union[str, HttpUrl]]] = Field(default=None, description="Private primary recipients")
+    cc: Optional[List[Union[str, HttpUrl]]] = Field(default=None, description="Secondary recipients")
+    bcc: Optional[List[Union[str, HttpUrl]]] = Field(default=None, description="Private secondary recipients")
+    media_type: Optional[str] = Field(default=None, description="MIME type")
+
+    @field_validator('media_type')
+    def validate_media_type(cls, value):
+        allowed_mimes = ["image/jpeg", "image/png", "application/json", "text/html", "video/mp4"]
+        if value and not value in allowed_mimes:
+            raise ValueError(f"Invalid MIME type: {value}")
+        return value
+
+    def is_public(self) -> bool:
+        """Check if the object is public."""
+        public_address = "https://www.w3.org/ns/activitystreams#Public"
+        return (
+            (isinstance(self.to, list) and public_address in self.to) or
+            (isinstance(self.cc, list) and public_address in self.cc)
+        )
+
+    def get_mentions(self) -> List[str]:
+        """Extract mentions from the object's tags."""
+        if not self.tag:
+            return []
+        return [
+            tag['href'] 
+            for tag in self.tag 
+            if isinstance(tag, dict) and tag.get('type') == "Mention"
+        ]
+
+    def __str__(self):
+        return str(self.id)
+
+class APEvent(APObject):
+    """Event object as defined in ActivityStreams 2.0.
+    
+    An Event represents any kind of event that can occur, such as a concert,
+    a meeting, or a conference.
+
+    Properties:
+        type (Literal["Event"]): Always set to "Event".
+        start_time (Optional[datetime]): When the event begins.
+        end_time (Optional[datetime]): When the event concludes.
+        location (Optional['APPlace']): Where the event takes place.
+
+    Validation:
+        - end_time must be after start_time if both are provided
+        - Inherits all validation from APObject
+
+    https://www.w3.org/TR/activitystreams-vocabulary/#event
+    """
+    type: Literal["Event"] = Field(default="Event", description="Indicates that this object represents an event")
+    start_time: Optional[datetime] = Field(default=None, description="The start time of the event")
+    end_time: Optional[datetime] = Field(default=None, description="The end time of the event")
+    location: Optional['APPlace'] = Field(default=None, description="The location of the event")
+
+    @model_validator(mode='after')
+    def validate_end_time(self) -> 'APEvent':
+        """Validate that end_time is after start_time if both are provided."""
+        if self.start_time and self.end_time:
+            if self.end_time <= self.start_time:
+                raise ValueError('end_time must be after start_time')
+        return self
+
+class APPlace(APObject):
+    """Place object as defined in ActivityStreams 2.0.
+    
+    A Place represents a physical or logical location. It can be used to represent
+    a geographic location, a venue, or any other kind of place.
+
+    Properties:
+        type (Literal["Place"]): Always set to "Place".
+        accuracy (Optional[float]): The accuracy of the coordinates (in meters).
+        altitude (Optional[float]): The altitude of the place (in meters).
+        latitude (Optional[float]): The latitude (-90 to 90).
+        longitude (Optional[float]): The longitude (-180 to 180).
+        radius (Optional[float]): The radius of uncertainty.
+        units (Optional[str]): The units for the radius (e.g., 'cm', 'feet', 'km').
+
+    Validation:
+        - latitude must be between -90 and 90
+        - longitude must be between -180 and 180
+        - units must be one of: 'cm', 'feet', 'inches', 'km', 'm', 'miles'
+        - Inherits all validation from APObject
+
+    https://www.w3.org/TR/activitystreams-vocabulary/#place
+    """
+    type: Literal["Place"] = Field(default="Place", description="Indicates that this object represents a place")
+    accuracy: Optional[float] = Field(default=None, description="The accuracy of the coordinates")
+    altitude: Optional[float] = Field(default=None, description="The altitude of the place")
+    latitude: Optional[float] = Field(default=None, ge=-90, le=90, description="The latitude of the place")
+    longitude: Optional[float] = Field(default=None, ge=-180, le=180, description="The longitude of the place")
+    radius: Optional[float] = Field(default=None, description="The radius of uncertainty")
+    units: Optional[str] = Field(default=None, description="The units for the radius")
+
+    @field_validator('units')
+    def validate_units(cls, v):
+        valid_units = ['cm', 'feet', 'inches', 'km', 'm', 'miles']
+        if v and v not in valid_units:
+            raise ValueError(f"Invalid unit: {v}. Must be one of {', '.join(valid_units)}")
+        return v
+
+class APProfile(APObject):
+    """
+    Represents a Profile object.
+
+    Attributes:
+        type (Literal["Profile"]): Always set to "Profile".
+        describes (Union[str, HttpUrl, APObject]): The object that this profile describes.
+
+    https://www.w3.org/TR/activitystreams-vocabulary/#profile
+    """
+    type: Literal["Profile"] = "Profile"
+    describes: Union[str, HttpUrl, APObject]
+
+class APRelationship(APObject):
+    """
+    Represents a Relationship between objects.
+
+    Attributes:
+        type (Literal["Relationship"]): Always set to "Relationship".
+        subject (Union[str, HttpUrl, APObject]): The subject of the relationship.
+        object (Union[str, HttpUrl, APObject]): The object of the relationship.
+        relationship (str): The type of relationship.
+
+    https://www.w3.org/TR/activitystreams-vocabulary/#relationship
+    """
+    type: Literal["Relationship"] = "Relationship"
+    subject: Union[str, HttpUrl, APObject]
+    object: Union[str, HttpUrl, APObject]
+    relationship: str
+
+class APTombstone(APObject):
+    """
+    Represents a Tombstone (deleted object).
+
+    Attributes:
+        type (Literal["Tombstone"]): Always set to "Tombstone".
+        former_type: str
+        deleted: datetime
+
+    https://www.w3.org/TR/activitystreams-vocabulary/#tombstone
+    """
+    type: Literal["Tombstone"] = "Tombstone"
+    former_type: str
+    deleted: datetime
+
+class APArticle(APObject):
+    """
+    Represents an Article.
+
+    Attributes:
+        type (Literal["Article"]): Always set to "Article".
+
+    https://www.w3.org/TR/activitystreams-vocabulary/#article
+    """
+    type: Literal["Article"] = "Article"
+
+class APAudio(APObject):
+    """
+    Represents an Audio object.
+
+    Attributes:
+        type (Literal["Audio"]): Always set to "Audio".
+        duration (Optional[str]): The duration of the audio.
+
+    https://www.w3.org/TR/activitystreams-vocabulary/#audio
+    """
+    type: Literal["Audio"] = "Audio"
+    duration: Optional[str] = None
+
+class APDocument(APObject):
+    """
+    Represents a Document.
+
+    Attributes:
+        type (Literal["Document"]): Always set to "Document".
+
+    https://www.w3.org/TR/activitystreams-vocabulary/#document
+    """
+    type: Literal["Document"] = "Document"
+
+class APImage(APObject):
+    """
+    Represents an Image.
+
+    Attributes:
+        type (Literal["Image"]): Always set to "Image".
+        width (Optional[int]): The width of the image.
+        height (Optional[int]): The height of the image.
+
+    https://www.w3.org/TR/activitystreams-vocabulary/#image
+    """
+    type: Literal["Image"] = "Image"
+    width: Optional[int] = Field(None, gt=0, description="The width of the image")
+    height: Optional[int] = Field(None, gt=0, description="The height of the image")
+
+class APNote(APObject):
+    """
+    Represents a Note.
+
+    Attributes:
+        type (Literal["Note"]): Always set to "Note".
+
+    https://www.w3.org/TR/activitystreams-vocabulary/#note
+    """
+    type: Literal["Note"] = "Note"
+
+class APPage(APObject):
+    """
+    Represents a Page.
+
+    Attributes:
+        type (Literal["Page"]): Always set to "Page".
+
+    https://www.w3.org/TR/activitystreams-vocabulary/#page
+    """
+    type: Literal["Page"] = "Page"
+
+class APVideo(APObject):
+    """
+    Represents a Video object.
+
+    Attributes:
+        type (Literal["Video"]): Always set to "Video".
+        duration (Optional[str]): The duration of the video.
+        media_type (Optional[str]): The media type of the video.
+
+    https://www.w3.org/TR/activitystreams-vocabulary/#video
+    """
+    type: Literal["Video"] = "Video"
+    duration: Optional[str] = None
diff --git a/src/pyfed/serializers/__init__.py b/src/pyfed/serializers/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..098602899a00a840ca7435fd0a9f2af5dccf6797
--- /dev/null
+++ b/src/pyfed/serializers/__init__.py
@@ -0,0 +1,3 @@
+from .json_serializer import ActivityPubSerializer, ActivityPubBase
+
+# __all__ = ["ActivityPubModel"]
\ No newline at end of file
diff --git a/src/pyfed/serializers/json_serializer.py b/src/pyfed/serializers/json_serializer.py
new file mode 100644
index 0000000000000000000000000000000000000000..3cbb28b73720c8153a5c4be7107a4dc89f9c2017
--- /dev/null
+++ b/src/pyfed/serializers/json_serializer.py
@@ -0,0 +1,113 @@
+"""
+json_serializer.py
+This module provides JSON serialization for ActivityPub objects.
+"""
+
+import json
+from datetime import datetime
+from typing import Any, Dict
+from pydantic import BaseModel
+from pydantic_core import Url
+from ..utils.logging import get_logger
+# from ..plugins import plugin_manager
+
+logger = get_logger(__name__)
+
+def to_camel_case(snake_str: str) -> str:
+    """Converts snake_case to camelCase."""
+    components = snake_str.split('_')
+    return components[0] + ''.join(x.title() for x in components[1:])
+
+def convert_dict_keys_to_camel_case(data: Dict[str, Any]) -> Dict[str, Any]:
+    """Recursively converts all dictionary keys from snake_case to camelCase."""
+    if not isinstance(data, dict):
+        return data
+    
+    return {
+        to_camel_case(key): (
+            convert_dict_keys_to_camel_case(value) if isinstance(value, (dict, list))
+            else value
+        )
+        for key, value in data.items()
+    }
+
+class ActivityPubJSONEncoder(json.JSONEncoder):
+    """Custom JSON encoder for ActivityPub objects."""
+    
+    def default(self, obj: Any) -> Any:
+        if isinstance(obj, BaseModel):
+            return convert_dict_keys_to_camel_case(obj.model_dump())
+        if isinstance(obj, Url):
+            return str(obj)
+        if isinstance(obj, datetime):
+            return obj.isoformat()
+        if isinstance(obj, list):
+            return [self.default(item) for item in obj]
+        return super().default(obj)
+
+class ActivityPubBase(BaseModel):
+    """Base class for all ActivityPub models."""
+    
+    class Config:
+        """Pydantic model configuration."""
+        populate_by_name = True
+        use_enum_values = True
+        alias_generator = to_camel_case
+
+class ActivityPubSerializer:
+    """Serializer for ActivityPub objects."""
+
+    @staticmethod
+    def serialize(obj: ActivityPubBase, include_context: bool = True, **kwargs) -> str:
+        """
+        Serialize an ActivityPub object to JSON string.
+
+        Args:
+            obj (ActivityPubBase): The object to serialize.
+            include_context (bool): Whether to include @context field.
+            **kwargs: Additional arguments passed to json.dumps.
+
+        Returns:
+            str: JSON string representation of the object.
+        """
+        logger.debug("Serializing object")
+        
+        # Execute pre-serialize hook
+        # plugin_manager.execute_hook('pre_serialize', obj)
+        
+        # Convert to dictionary and convert keys to camelCase
+        data = convert_dict_keys_to_camel_case(obj.model_dump())
+        
+        # Add context if needed
+        if include_context:
+            data["@context"] = "https://www.w3.org/ns/activitystreams"
+        
+        # Serialize to JSON
+        serialized = json.dumps(data, cls=ActivityPubJSONEncoder, **kwargs)
+        
+        return serialized
+
+    @staticmethod
+    def deserialize(json_str: str, model_class: type[ActivityPubBase]) -> ActivityPubBase:
+        """
+        Deserialize a JSON string to an ActivityPub object.
+
+        Args:
+            json_str (str): The JSON string to deserialize.
+            model_class (type[ActivityPubBase]): The class to deserialize into.
+
+        Returns:
+            ActivityPubBase: The deserialized object.
+        """
+        logger.debug(f"Deserializing to {model_class.__name__}")
+        
+        # Parse JSON
+        data = json.loads(json_str)
+        
+        # Remove context if present
+        data.pop("@context", None)
+        
+        # Create object
+        obj = model_class.model_validate(data)
+        
+        return obj
diff --git a/src/pyfed/utils/exceptions.py b/src/pyfed/utils/exceptions.py
new file mode 100644
index 0000000000000000000000000000000000000000..f9cbc9caf1978486ad446950910175384a3bec0f
--- /dev/null
+++ b/src/pyfed/utils/exceptions.py
@@ -0,0 +1,42 @@
+class ActivityPubException(Exception):
+    """Base exception for ActivityPub-related errors."""
+
+class ValidationError(ActivityPubException):
+    """Raised when validation fails."""
+
+class DeserializationError(ActivityPubException):
+    """Raised when deserialization fails."""
+
+class RemoteObjectFetchError(ActivityPubException):
+    """Raised when fetching a remote object fails."""
+
+class InvalidURLError(ValidationError):
+    """Raised when an invalid URL is provided."""
+
+class InvalidDateTimeError(ValidationError):
+    """Raised when an invalid date/time is provided."""
+
+class SignatureVerificationError(ActivityPubException):
+    """Raised when signature verification fails."""
+    pass
+
+class SignatureError(ActivityPubException):
+    """Raised when signature verification fails."""
+    pass
+
+class AuthenticationError(ActivityPubException):
+    """Raised when signature verification fails."""
+    pass
+
+class RateLimitExceeded(ActivityPubException):
+    """Raised when signature verification fails."""
+    pass
+
+class WebFingerError(ActivityPubException):
+    """Raised when signature verification fails."""
+    pass
+
+class SecurityError(ActivityPubException):
+    """Raised when signature verification fails."""
+    pass
+
diff --git a/src/pyfed/utils/logging.py b/src/pyfed/utils/logging.py
new file mode 100644
index 0000000000000000000000000000000000000000..31e4021a45cf0e1d6c8762c589a4b0f1d1db7f06
--- /dev/null
+++ b/src/pyfed/utils/logging.py
@@ -0,0 +1,36 @@
+import logging
+import sys
+from typing import Optional
+
+def configure_logging(level: Optional[str] = None):
+    """
+    Configure the logging system for the ActivityPub library.
+
+    Args:
+        level (Optional[str]): The logging level. If None, defaults to INFO.
+    """
+    if level is None:
+        level = logging.INFO
+    else:
+        level = getattr(logging, level.upper())
+
+    logging.basicConfig(
+        level=level,
+        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+        handlers=[
+            logging.StreamHandler(sys.stdout),
+            logging.FileHandler('activitypub_library.log')
+        ]
+    )
+
+def get_logger(name: str) -> logging.Logger:
+    """
+    Get a logger with the specified name.
+
+    Args:
+        name (str): The name of the logger, typically __name__ of the module.
+
+    Returns:
+        logging.Logger: A configured logger instance.
+    """
+    return logging.getLogger(name)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000000000000000000000000000000000000..50159df17aa7631645848fa6df4f565f24ee3a1d
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,34 @@
+import pytest
+from pathlib import Path
+import subprocess
+
+"""
+PyTest configuration and fixtures.
+"""
+
+pytest_plugins = ('pytest_asyncio',)
+
+@pytest.fixture(scope="session", autouse=True)
+def test_keys():
+    """Ensure test keys exist."""
+    keys_dir = Path("tests/fixtures/keys")
+    private_key = keys_dir / "private.pem"
+    public_key = keys_dir / "public.pem"
+    
+    if not private_key.exists() or not public_key.exists():
+        # Generate keys if they don't exist
+        subprocess.run(["python", "tests/fixtures/generate_keys.py"], check=True)
+    
+    return {
+        "private_key": str(private_key),
+        "public_key": str(public_key)
+    }
+
+@pytest.fixture
+def signature_verifier(test_keys):
+    """Create a test signature verifier with generated keys."""
+    from pyfed.security.http_signatures import HTTPSignatureVerifier
+    return HTTPSignatureVerifier(
+        private_key_path=test_keys["private_key"],
+        public_key_path=test_keys["public_key"]
+    )
\ No newline at end of file
diff --git a/tests/pytest.ini b/tests/pytest.ini
new file mode 100644
index 0000000000000000000000000000000000000000..138df0988d74e9648ba05623b89a4f7c53d80fae
--- /dev/null
+++ b/tests/pytest.ini
@@ -0,0 +1,4 @@
+[pytest]  
+filterwarnings =  
+    ignore::DeprecationWarning
+pythonpath = ../src
\ No newline at end of file
diff --git a/tests/tests_models/__init__.py b/tests/tests_models/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/tests_models/test_activities.py b/tests/tests_models/test_activities.py
new file mode 100644
index 0000000000000000000000000000000000000000..544ec39e5db8e1e3a61b1e6dd0507f6f36232031
--- /dev/null
+++ b/tests/tests_models/test_activities.py
@@ -0,0 +1,146 @@
+"""
+test_activities.py
+This module contains tests for the Activity types in ActivityPub.
+"""
+import pytest
+from datetime import datetime
+from pydantic import ValidationError
+from pyfed.models import (
+    APCreate, APUpdate, APDelete, APFollow,
+    APUndo, APLike, APAnnounce
+)
+
+def test_create_activity():
+    """Test creating a Create activity."""
+    activity = APCreate(
+        id="https://example.com/activity/123",
+        type="Create",
+        actor="https://example.com/user/1",
+        object={
+            "id": "https://example.com/note/123",
+            "type": "Note",
+            "content": "Hello, World!"
+        }
+    )
+    assert activity.type == "Create"
+    assert str(activity.actor) == "https://example.com/user/1"
+    assert activity.object["type"] == "Note"
+
+def test_update_activity():
+    """Test creating an Update activity."""
+    activity = APUpdate(
+        id="https://example.com/activity/123",
+        type="Update",
+        actor="https://example.com/user/1",
+        object={
+            "id": "https://example.com/note/123",
+            "type": "Note",
+            "content": "Updated content"
+        }
+    )
+    assert activity.type == "Update"
+    assert activity.object["content"] == "Updated content"
+
+def test_delete_activity():
+    """Test creating a Delete activity."""
+    activity = APDelete(
+        id="https://example.com/activity/123",
+        type="Delete",
+        actor="https://example.com/user/1",
+        object="https://example.com/note/123"
+    )
+    assert activity.type == "Delete"
+    assert str(activity.object) == "https://example.com/note/123"
+
+def test_follow_activity():
+    """Test creating a Follow activity."""
+    activity = APFollow(
+        id="https://example.com/activity/123",
+        type="Follow",
+        actor="https://example.com/user/1",
+        object="https://example.com/user/2"
+    )
+    assert activity.type == "Follow"
+    assert str(activity.object) == "https://example.com/user/2"
+
+def test_undo_activity():
+    """Test creating an Undo activity."""
+    activity = APUndo(
+        id="https://example.com/activity/123",
+        type="Undo",
+        actor="https://example.com/user/1",
+        object={
+            "id": "https://example.com/activity/456",
+            "type": "Follow",
+            "actor": "https://example.com/user/1",
+            "object": "https://example.com/user/2"
+        }
+    )
+    assert activity.type == "Undo"
+    assert activity.object["type"] == "Follow"
+
+def test_like_activity():
+    """Test creating a Like activity."""
+    activity = APLike(
+        id="https://example.com/activity/123",
+        type="Like",
+        actor="https://example.com/user/1",
+        object="https://example.com/note/123"
+    )
+    assert activity.type == "Like"
+    assert str(activity.object) == "https://example.com/note/123"
+
+def test_announce_activity():
+    """Test creating an Announce activity."""
+    activity = APAnnounce(
+        id="https://example.com/activity/123",
+        type="Announce",
+        actor="https://example.com/user/1",
+        object="https://example.com/note/123"
+    )
+    assert activity.type == "Announce"
+    assert str(activity.object) == "https://example.com/note/123"
+
+def test_activity_with_target():
+    """Test creating an activity with a target."""
+    activity = APCreate(
+        id="https://example.com/activity/123",
+        type="Create",
+        actor="https://example.com/user/1",
+        object="https://example.com/note/123",
+        target="https://example.com/collection/1"
+    )
+    assert str(activity.target) == "https://example.com/collection/1"
+
+def test_activity_with_result():
+    """Test creating an activity with a result."""
+    activity = APCreate(
+        id="https://example.com/activity/123",
+        type="Create",
+        actor="https://example.com/user/1",
+        object="https://example.com/note/123",
+        result={
+            "id": "https://example.com/result/1",
+            "type": "Note",
+            "content": "Result content"
+        }
+    )
+    assert activity.result["type"] == "Note"
+
+def test_invalid_activity_missing_actor():
+    """Test that activity creation fails when actor is missing."""
+    with pytest.raises(ValidationError):
+        APCreate(
+            id="https://example.com/activity/123",
+            type="Create",
+            object="https://example.com/note/123"
+        )
+
+def test_invalid_activity_missing_object():
+    """Test that activity creation fails when object is missing for non-intransitive activities."""
+    with pytest.raises(ValidationError):
+        APCreate(
+            id="https://example.com/activity/123",
+            type="Create",
+            actor="https://example.com/user/1"
+        )
diff --git a/tests/tests_models/test_actors.py b/tests/tests_models/test_actors.py
new file mode 100644
index 0000000000000000000000000000000000000000..29659a4fe627d1f3d6e9a44ed8fec3dfa69d7624
--- /dev/null
+++ b/tests/tests_models/test_actors.py
@@ -0,0 +1,135 @@
+"""
+test_actors.py
+This module contains tests for the Actor types in ActivityPub.
+"""
+import pytest
+from datetime import datetime
+from pydantic import ValidationError
+from pyfed.models import (
+    APPerson, APGroup, APOrganization, APApplication, APService
+)
+
+def test_valid_person():
+    person = APPerson(
+        id="https://example.com/users/alice",
+        type="Person",
+        name="Alice",
+        inbox="https://example.com/users/alice/inbox",
+        outbox="https://example.com/users/alice/outbox",
+        preferred_username="alice"
+    )
+    assert person.type == "Person"
+    assert str(person.inbox) == "https://example.com/users/alice/inbox"
+    assert str(person.outbox) == "https://example.com/users/alice/outbox"
+    assert person.preferred_username == "alice"
+
+def test_person_with_optional_fields():
+    person = APPerson(
+        id="https://example.com/users/alice",
+        type="Person",
+        name="Alice",
+        inbox="https://example.com/users/alice/inbox",
+        outbox="https://example.com/users/alice/outbox",
+        following="https://example.com/users/alice/following",
+        followers="https://example.com/users/alice/followers",
+        liked="https://example.com/users/alice/liked",
+        streams=["https://example.com/users/alice/stream1"]
+    )
+    assert str(person.following) == "https://example.com/users/alice/following"
+    assert str(person.followers) == "https://example.com/users/alice/followers"
+    assert str(person.liked) == "https://example.com/users/alice/liked"
+    assert len(person.streams) == 1
+
+def test_invalid_person_missing_required():
+    with pytest.raises(ValidationError):
+        APPerson(
+            id="https://example.com/users/alice",
+            type="Person",
+            name="Alice"
+            # Missing required inbox and outbox
+        )
+
+def test_invalid_person_invalid_url():
+    with pytest.raises(ValidationError):
+        APPerson(
+            id="https://example.com/users/alice",
+            type="Person",
+            name="Alice",
+            inbox="not-a-url",  # Invalid URL
+            outbox="https://example.com/users/alice/outbox"
+        )
+
+def test_valid_group():
+    group = APGroup(
+        id="https://example.com/groups/admins",
+        type="Group",
+        name="Administrators",
+        inbox="https://example.com/groups/admins/inbox",
+        outbox="https://example.com/groups/admins/outbox"
+    )
+    assert group.type == "Group"
+    assert group.name == "Administrators"
+
+def test_valid_organization():
+    org = APOrganization(
+        id="https://example.com/org/acme",
+        type="Organization",
+        name="ACME Corporation",
+        inbox="https://example.com/org/acme/inbox",
+        outbox="https://example.com/org/acme/outbox"
+    )
+    assert org.type == "Organization"
+    assert org.name == "ACME Corporation"
+
+def test_valid_application():
+    app = APApplication(
+        id="https://example.com/apps/bot",
+        type="Application",
+        name="Bot Application",
+        inbox="https://example.com/apps/bot/inbox",
+        outbox="https://example.com/apps/bot/outbox"
+    )
+    assert app.type == "Application"
+    assert app.name == "Bot Application"
+
+def test_valid_service():
+    service = APService(
+        id="https://example.com/services/api",
+        type="Service",
+        name="API Service",
+        inbox="https://example.com/services/api/inbox",
+        outbox="https://example.com/services/api/outbox"
+    )
+    assert service.type == "Service"
+    assert service.name == "API Service"
+
+def test_actor_with_public_key():
+    person = APPerson(
+        id="https://example.com/users/alice",
+        type="Person",
+        name="Alice",
+        inbox="https://example.com/users/alice/inbox",
+        outbox="https://example.com/users/alice/outbox",
+        public_key={
+            "id": "https://example.com/users/alice#main-key",
+            "owner": "https://example.com/users/alice",
+            "publicKeyPem": "-----BEGIN PUBLIC KEY-----\n..."
+        }
+    )
+    assert person.public_key is not None
+    assert "publicKeyPem" in person.public_key
+
+def test_actor_with_endpoints():
+    person = APPerson(
+        id="https://example.com/users/alice",
+        type="Person",
+        name="Alice",
+        inbox="https://example.com/users/alice/inbox",
+        outbox="https://example.com/users/alice/outbox",
+        endpoints={
+            "sharedInbox": "https://example.com/inbox",
+            "oauthAuthorizationEndpoint": "https://example.com/oauth/authorize"
+        }
+    )
+    assert person.endpoints is not None
+    assert "sharedInbox" in person.endpoints
diff --git a/tests/tests_models/test_collections.py b/tests/tests_models/test_collections.py
new file mode 100644
index 0000000000000000000000000000000000000000..1d715263452da32f6a3e294069565da76a013224
--- /dev/null
+++ b/tests/tests_models/test_collections.py
@@ -0,0 +1,126 @@
+"""
+test_collections.py
+This module contains tests for the Collection types in ActivityPub.
+"""
+import pytest
+from pydantic import ValidationError
+from pyfed.models import (
+    APCollection, APOrderedCollection,
+    APCollectionPage, APOrderedCollectionPage
+)
+
+def test_valid_collection():
+    """Test creating a valid Collection."""
+    collection = APCollection(
+        id="https://example.com/collection/123",
+        type="Collection",
+        total_items=10,
+        items=["https://example.com/item/1", "https://example.com/item/2"]
+    )
+    assert collection.type == "Collection"
+    assert collection.total_items == 10
+    assert len(collection.items) == 2
+
+def test_collection_with_optional_fields():
+    """Test creating a Collection with all optional fields."""
+    collection = APCollection(
+        id="https://example.com/collection/123",
+        type="Collection",
+        total_items=2,
+        current="https://example.com/collection/123/current",
+        first="https://example.com/collection/123/first",
+        last="https://example.com/collection/123/last",
+        items=["https://example.com/item/1", "https://example.com/item/2"]
+    )
+    assert str(collection.current) == "https://example.com/collection/123/current"
+    assert str(collection.first) == "https://example.com/collection/123/first"
+    assert str(collection.last) == "https://example.com/collection/123/last"
+
+def test_invalid_collection_negative_total():
+    """Test that Collection creation fails with negative total_items."""
+    with pytest.raises(ValidationError):
+        APCollection(
+            id="https://example.com/collection/123",
+            type="Collection",
+            total_items=-1
+        )
+
+def test_valid_ordered_collection():
+    """Test creating a valid OrderedCollection."""
+    collection = APOrderedCollection(
+        id="https://example.com/collection/123",
+        type="OrderedCollection",
+        total_items=2,
+        ordered_items=["https://example.com/item/1", "https://example.com/item/2"]
+    )
+    assert collection.type == "OrderedCollection"
+    assert len(collection.ordered_items) == 2
+
+def test_valid_collection_page():
+    """Test creating a valid CollectionPage."""
+    page = APCollectionPage(
+        id="https://example.com/collection/123/page/1",
+        type="CollectionPage",
+        part_of="https://example.com/collection/123",
+        items=["https://example.com/item/1"]
+    )
+    assert page.type == "CollectionPage"
+    assert str(page.part_of) == "https://example.com/collection/123"
+
+def test_collection_page_with_navigation():
+    """Test creating a CollectionPage with navigation links."""
+    page = APCollectionPage(
+        id="https://example.com/collection/123/page/2",
+        type="CollectionPage",
+        part_of="https://example.com/collection/123",
+        items=["https://example.com/item/2"],
+        next="https://example.com/collection/123/page/3",
+        prev="https://example.com/collection/123/page/1"
+    )
+    assert str(page.next) == "https://example.com/collection/123/page/3"
+    assert str(page.prev) == "https://example.com/collection/123/page/1"
+
+def test_valid_ordered_collection_page():
+    """Test creating a valid OrderedCollectionPage."""
+    page = APOrderedCollectionPage(
+        id="https://example.com/collection/123/page/1",
+        type="OrderedCollectionPage",
+        part_of="https://example.com/collection/123",
+        ordered_items=["https://example.com/item/1"],
+        start_index=0
+    )
+    assert page.type == "OrderedCollectionPage"
+    assert page.start_index == 0
+
+def test_invalid_ordered_page_negative_index():
+    """Test that OrderedCollectionPage creation fails with negative start_index."""
+    with pytest.raises(ValidationError):
+        APOrderedCollectionPage(
+            id="https://example.com/collection/123/page/1",
+            type="OrderedCollectionPage",
+            start_index=-1
+        )
+
+def test_collection_with_object_items():
+    """Test creating a Collection with APObject items."""
+    collection = APCollection(
+        id="https://example.com/collection/123",
+        type="Collection",
+        items=[{
+            "id": "https://example.com/item/1",
+            "type": "Note",
+            "content": "Test note"
+        }]
+    )
+    assert len(collection.items) == 1
+    assert collection.items[0]["type"] == "Note"
+
+def test_ordered_collection_empty():
+    """Test creating an empty OrderedCollection."""
+    collection = APOrderedCollection(
+        id="https://example.com/collection/123",
+        type="OrderedCollection",
+        total_items=0
+    )
+    assert collection.total_items == 0
+    assert collection.ordered_items is None
diff --git a/tests/tests_models/test_factories.py b/tests/tests_models/test_factories.py
new file mode 100644
index 0000000000000000000000000000000000000000..1b00439cd1768a7e393bce9c230c26b1132a7619
--- /dev/null
+++ b/tests/tests_models/test_factories.py
@@ -0,0 +1,38 @@
+"""
+test_factories.py
+This module contains tests for the factories of the pyfed package.
+"""
+import factory
+from factory.fuzzy import FuzzyText, FuzzyFloat
+from pyfed.models import APObject, APEvent, APPlace
+
+class APObjectFactory(factory.Factory):
+    class Meta:
+        model = APObject
+
+    id = factory.Sequence(lambda n: f"https://example.com/object/{n}")
+    type = "Object"
+    name = FuzzyText()
+    content = FuzzyText(length=200)
+
+class APPlaceFactory(factory.Factory):
+    class Meta:
+        model = APPlace
+
+    id = factory.Sequence(lambda n: f"https://example.com/place/{n}")
+    type = "Place"
+    name = FuzzyText()
+    latitude = FuzzyFloat(-90, 90)
+    longitude = FuzzyFloat(-180, 180)
+
+class APEventFactory(factory.Factory):
+    class Meta:
+        model = APEvent
+
+    id = factory.Sequence(lambda n: f"https://example.com/event/{n}")
+    type = "Event"
+    name = FuzzyText()
+    location = factory.SubFactory(APPlaceFactory)
+
+# Usage example:
+# event = APEventFactory()
diff --git a/tests/tests_models/test_imports.py b/tests/tests_models/test_imports.py
new file mode 100644
index 0000000000000000000000000000000000000000..e663a270c5cd134abb1ce9b586f16c226e3350ba
--- /dev/null
+++ b/tests/tests_models/test_imports.py
@@ -0,0 +1,71 @@
+"""
+test_imports.py
+This module tests that all necessary modules can be imported correctly.
+"""
+
+import pytest
+from pyfed.serializers.json_serializer import ActivityPubSerializer
+# from pyfed.plugins import plugin_manager  # Updated import path
+from pyfed.models import (
+    APObject, APEvent, APPlace, APProfile, APRelationship, APTombstone,
+    APArticle, APAudio, APDocument, APImage, APNote, APPage, APVideo,
+    APActor, APPerson, APGroup, APOrganization, APApplication, APService,
+    APLink, APMention,
+    APCollection, APOrderedCollection, APCollectionPage, APOrderedCollectionPage,
+    APCreate, APUpdate, APDelete, APFollow, APUndo, APLike, APAnnounce
+)
+
+def test_can_import_models():
+    """Test that all models can be imported."""
+    assert APObject
+    assert APEvent
+    assert APPlace
+    assert APProfile
+    assert APRelationship
+    assert APTombstone
+    assert APArticle
+    assert APAudio
+    assert APDocument
+    assert APImage
+    assert APNote
+    assert APPage
+    assert APVideo
+
+def test_can_import_actors():
+    """Test that all actor types can be imported."""
+    assert APActor
+    assert APPerson
+    assert APGroup
+    assert APOrganization
+    assert APApplication
+    assert APService
+
+def test_can_import_links():
+    """Test that all link types can be imported."""
+    assert APLink
+    assert APMention
+
+def test_can_import_collections():
+    """Test that all collection types can be imported."""
+    assert APCollection
+    assert APOrderedCollection
+    assert APCollectionPage
+    assert APOrderedCollectionPage
+
+def test_can_import_activities():
+    """Test that all activity types can be imported."""
+    assert APCreate
+    assert APUpdate
+    assert APDelete
+    assert APFollow
+    assert APUndo
+    assert APLike
+    assert APAnnounce
+
+def test_can_import_serializer():
+    """Test that the serializer can be imported."""
+    assert ActivityPubSerializer
+
+# def test_can_import_plugin_manager():
+#     """Test that the plugin manager can be imported."""
+#     assert plugin_manager
diff --git a/tests/tests_models/test_interactions.py b/tests/tests_models/test_interactions.py
new file mode 100644
index 0000000000000000000000000000000000000000..3464332d56a7c0627fb8212c63300e6054c63344
--- /dev/null
+++ b/tests/tests_models/test_interactions.py
@@ -0,0 +1,395 @@
+"""
+test_interactions.py
+This module contains tests for the interactions between ActivityPub models.
+"""
+import pytest
+from datetime import datetime, timedelta
+from pyfed.models import (
+    APEvent, APPlace, APPerson, APNote, APObject, APImage,
+    APCollection, APOrderedCollection, APLink, APMention,
+    APCreate, APLike, APFollow, APAnnounce, APUpdate, APDelete,
+    APUndo, APDocument, APGroup, APOrganization, APOrderedCollectionPage,
+    APRelationship
+)
+
+def test_event_with_place_and_attendees():
+    """Test creating an event with a location and attendees."""
+    place = APPlace(
+        id="https://example.com/place/123",
+        name="Event Venue",
+        latitude=52.5200,
+        longitude=13.4050
+    )
+    attendee1 = APPerson(
+        id="https://example.com/person/1",
+        name="John Doe",
+        inbox="https://example.com/person/1/inbox",
+        outbox="https://example.com/person/1/outbox"
+    )
+    attendee2 = APPerson(
+        id="https://example.com/person/2",
+        name="Jane Doe",
+        inbox="https://example.com/person/2/inbox",
+        outbox="https://example.com/person/2/outbox"
+    )
+    event = APEvent(
+        id="https://example.com/event/123",
+        name="Test Event",
+        location=place,
+        to=[str(attendee1.id), str(attendee2.id)]
+    )
+    assert str(event.location.id) == "https://example.com/place/123"
+    assert event.location.name == "Event Venue"
+    assert len(event.to) == 2
+
+def test_note_with_mentions_and_attachments():
+    """Test creating a note with mentions and attachments."""
+    mentioned_person = APPerson(
+        id="https://example.com/person/1",
+        name="John Doe",
+        inbox="https://example.com/person/1/inbox",
+        outbox="https://example.com/person/1/outbox"
+    )
+    image = APImage(
+        id="https://example.com/image/1",
+        type="Image",
+        url="https://example.com/image.jpg",
+        width=800,
+        height=600
+    )
+    note = APNote(
+        id="https://example.com/note/123",
+        content="Hello @John! Check out this image.",
+        tag=[{"type": "Mention", "href": str(mentioned_person.id)}],
+        attachment=[image]
+    )
+    assert len(note.tag) == 1
+    assert len(note.attachment) == 1
+    assert isinstance(note.attachment[0], APImage)
+
+def test_collection_with_pagination():
+    """Test creating a collection with pagination."""
+    items = [
+        APNote(
+            id=f"https://example.com/note/{i}",
+            type="Note",
+            content=f"Note {i}"
+        ) for i in range(1, 6)
+    ]
+    collection = APCollection(
+        id="https://example.com/collection/123",
+        type="Collection",
+        total_items=len(items),
+        items=items,
+        first="https://example.com/collection/123?page=1",
+        last="https://example.com/collection/123?page=1"
+    )
+    assert collection.total_items == 5
+    assert len(collection.items) == 5
+    assert str(collection.first).endswith("page=1")
+
+def test_create_activity_with_note():
+    """Test creating a Create activity with a note."""
+    author = APPerson(
+        id="https://example.com/person/1",
+        name="John Doe",
+        inbox="https://example.com/person/1/inbox",
+        outbox="https://example.com/person/1/outbox"
+    )
+    note = APNote(
+        id="https://example.com/note/123",
+        content="Hello, World!"
+    )
+    activity = APCreate(
+        id="https://example.com/activity/123",
+        actor=author,
+        object=note,
+        to=["https://www.w3.org/ns/activitystreams#Public"]
+    )
+    assert activity.type == "Create"
+    assert activity.is_public()
+    assert isinstance(activity.object, APNote)
+
+def test_like_and_announce_interaction():
+    """Test liking and announcing an object."""
+    original_note = APNote(
+        id="https://example.com/note/123",
+        content="Original content"
+    )
+    liker = APPerson(
+        id="https://example.com/person/1",
+        inbox="https://example.com/person/1/inbox",
+        outbox="https://example.com/person/1/outbox"
+    )
+    announcer = APPerson(
+        id="https://example.com/person/2",
+        inbox="https://example.com/person/2/inbox",
+        outbox="https://example.com/person/2/outbox"
+    )
+    
+    like = APLike(
+        id="https://example.com/activity/like/123",
+        actor=liker,
+        object=original_note
+    )
+    announce = APAnnounce(
+        id="https://example.com/activity/announce/123",
+        actor=announcer,
+        object=original_note
+    )
+    
+    assert like.type == "Like"
+    assert announce.type == "Announce"
+    assert str(like.object.id) == str(original_note.id)
+    assert str(announce.object.id) == str(original_note.id)
+
+def test_follow_with_collections():
+    """Test following relationship with collections."""
+    follower = APPerson(
+        id="https://example.com/person/1",
+        name="Follower",
+        inbox="https://example.com/person/1/inbox",
+        outbox="https://example.com/person/1/outbox",
+        following="https://example.com/person/1/following",
+        followers="https://example.com/person/1/followers"
+    )
+    followed = APPerson(
+        id="https://example.com/person/2",
+        name="Followed",
+        inbox="https://example.com/person/2/inbox",
+        outbox="https://example.com/person/2/outbox",
+        following="https://example.com/person/2/following",
+        followers="https://example.com/person/2/followers"
+    )
+    
+    follow = APFollow(
+        id="https://example.com/activity/follow/123",
+        actor=follower,
+        object=followed
+    )
+    
+    assert follow.type == "Follow"
+    assert str(follow.actor.id) == str(follower.id)
+    assert str(follow.object.id) == str(followed.id)
+    assert follower.following is not None
+    assert followed.followers is not None
+
+def test_mention_in_content():
+    """Test mentioning users in content with links."""
+    mentioned = APPerson(
+        id="https://example.com/person/1",
+        name="John Doe",
+        preferred_username="john",
+        inbox="https://example.com/person/1/inbox",
+        outbox="https://example.com/person/1/outbox"
+    )
+    
+    mention = APMention(
+        id="https://example.com/mention/123",
+        type="Mention",
+        href=mentioned.id,
+        name=f"@{mentioned.preferred_username}"
+    )
+    
+    note = APNote(
+        id="https://example.com/note/123",
+        content=f"Hello {mention.name}!",
+        tag=[mention]
+    )
+    
+    assert mention in note.tag
+    assert mention.name in note.content
+    assert str(mention.href) == str(mentioned.id)
+
+def test_collection_pagination_interaction():
+    """Test interaction between collections and their pages."""
+    items = [
+        APNote(
+            id=f"https://example.com/note/{i}",
+            type="Note",
+            content=f"Note {i}"
+        ) for i in range(1, 11)
+    ]
+    
+    collection = APOrderedCollection(
+        id="https://example.com/collection/123",
+        type="OrderedCollection",
+        total_items=len(items),
+        ordered_items=items[:5]  # First page
+    )
+    
+    page = APOrderedCollectionPage(
+        id="https://example.com/collection/123/page/1",
+        type="OrderedCollectionPage",
+        part_of=collection.id,
+        ordered_items=items[5:],  # Second page
+        start_index=5
+    )
+    
+    assert collection.total_items == 10
+    assert len(collection.ordered_items) == 5
+    assert len(page.ordered_items) == 5
+    assert page.start_index == 5
+
+def test_actor_relationships():
+    """Test complex relationships between actors."""
+    organization = APOrganization(
+        id="https://example.com/org/1",
+        name="Example Org",
+        inbox="https://example.com/org/1/inbox",
+        outbox="https://example.com/org/1/outbox"
+    )
+    
+    group = APGroup(
+        id="https://example.com/group/1",
+        name="Dev Team",
+        inbox="https://example.com/group/1/inbox",
+        outbox="https://example.com/group/1/outbox"
+    )
+    
+    member = APPerson(
+        id="https://example.com/person/1",
+        name="John Developer",
+        inbox="https://example.com/person/1/inbox",
+        outbox="https://example.com/person/1/outbox"
+    )
+    
+    relationship = APRelationship(
+        id="https://example.com/relationship/1",
+        type="Relationship",
+        subject=member.id,
+        object=group.id,
+        relationship="member"
+    )
+    
+    assert relationship.relationship == "member"
+    assert str(relationship.subject) == str(member.id)
+    assert str(relationship.object) == str(group.id)
+
+def test_content_with_multiple_attachments():
+    """Test creating content with multiple types of attachments."""
+    image = APImage(
+        id="https://example.com/image/1",
+        type="Image",
+        url="https://example.com/image.jpg",
+        width=800,
+        height=600
+    )
+    
+    document = APDocument(
+        id="https://example.com/document/1",
+        type="Document",
+        name="Specification",
+        url="https://example.com/spec.pdf"
+    )
+    
+    note = APNote(
+        id="https://example.com/note/1",
+        content="Check out these attachments!",
+        attachment=[image, document]
+    )
+    
+    assert len(note.attachment) == 2
+    assert isinstance(note.attachment[0], APImage)
+    assert isinstance(note.attachment[1], APDocument)
+
+def test_event_series():
+    """Test creating a series of related events."""
+    venue = APPlace(
+        id="https://example.com/place/1",
+        name="Conference Center",
+        latitude=51.5074,
+        longitude=-0.1278
+    )
+    
+    events = [
+        APEvent(
+            id=f"https://example.com/event/{i}",
+            name=f"Workshop Day {i}",
+            location=venue,
+            start_time=(datetime.now() + timedelta(days=i)).isoformat(),
+            end_time=(datetime.now() + timedelta(days=i, hours=2)).isoformat()
+        ) for i in range(1, 4)
+    ]
+    
+    collection = APOrderedCollection(
+        id="https://example.com/collection/workshop-series",
+        type="OrderedCollection",
+        name="Workshop Series",
+        ordered_items=events
+    )
+    
+    assert len(collection.ordered_items) == 3
+    assert all(isinstance(event, APEvent) for event in collection.ordered_items)
+    assert all(event.location.id == venue.id for event in collection.ordered_items)
+
+def test_nested_replies():
+    """Test handling nested replies to content."""
+    original_note = APNote(
+        id="https://example.com/note/1",
+        content="Original post"
+    )
+    
+    reply1 = APNote(
+        id="https://example.com/note/2",
+        content="First reply",
+        in_reply_to=str(original_note.id)
+    )
+    
+    reply2 = APNote(
+        id="https://example.com/note/3",
+        content="Reply to reply",
+        in_reply_to=str(reply1.id)
+    )
+    
+    assert str(reply1.in_reply_to) == str(original_note.id)
+    assert str(reply2.in_reply_to) == str(reply1.id)
+
+def test_activity_chain():
+    """Test a chain of related activities."""
+    author = APPerson(
+        id="https://example.com/person/1",
+        name="Author",
+        inbox="https://example.com/person/1/inbox",
+        outbox="https://example.com/person/1/outbox"
+    )
+    
+    # Create a note
+    note = APNote(
+        id="https://example.com/note/1",
+        content="Original content"
+    )
+    
+    create = APCreate(
+        id="https://example.com/activity/1",
+        actor=author,
+        object=note
+    )
+    
+    # Update the note
+    note.content = "Updated content"
+    update = APUpdate(
+        id="https://example.com/activity/2",
+        actor=author,
+        object=note
+    )
+    
+    # Delete the note
+    delete = APDelete(
+        id="https://example.com/activity/3",
+        actor=author,
+        object=note.id
+    )
+    
+    # Undo the deletion
+    undo = APUndo(
+        id="https://example.com/activity/4",
+        actor=author,
+        object=delete
+    )
+    
+    assert create.type == "Create"
+    assert update.type == "Update"
+    assert delete.type == "Delete"
+    assert undo.type == "Undo"
+    assert str(undo.object.id) == str(delete.id)
diff --git a/tests/tests_models/test_links.py b/tests/tests_models/test_links.py
new file mode 100644
index 0000000000000000000000000000000000000000..40a3f3f7561fa972496853d85712196cb8f63fba
--- /dev/null
+++ b/tests/tests_models/test_links.py
@@ -0,0 +1,96 @@
+"""
+test_links_models.py
+This module contains tests for the Link types in ActivityPub.
+"""
+import pytest
+from pydantic import ValidationError
+from pyfed.models import APLink, APMention
+
+def test_valid_link():
+    """Test creating a valid Link object."""
+    link = APLink(
+        id="https://example.com/link/123",
+        type="Link",
+        href="https://example.com/resource"
+    )
+    assert link.type == "Link"
+    assert str(link.href) == "https://example.com/resource"
+
+def test_link_with_optional_fields():
+    """Test creating a Link with all optional fields."""
+    link = APLink(
+        id="https://example.com/link/123",
+        type="Link",
+        href="https://example.com/resource",
+        name="Test Link",
+        hreflang="en",
+        media_type="image/jpeg",
+        width=800,
+        height=600,
+        preview="https://example.com/preview"
+    )
+    assert link.name == "Test Link"
+    assert link.hreflang == "en"
+    assert link.media_type == "image/jpeg"
+    assert link.width == 800
+    assert link.height == 600
+    assert str(link.preview) == "https://example.com/preview"
+
+def test_link_with_rel():
+    """Test creating a Link with relationship fields."""
+    link = APLink(
+        id="https://example.com/link/123",
+        type="Link",
+        href="https://example.com/resource",
+        rel=["canonical", "alternate"]
+    )
+    assert "canonical" in link.rel
+    assert "alternate" in link.rel
+
+def test_valid_mention():
+    """Test creating a valid Mention object."""
+    mention = APMention(
+        id="https://example.com/mention/123",
+        type="Mention",
+        href="https://example.com/user/alice",
+        name="@alice"
+    )
+    assert mention.type == "Mention"
+    assert str(mention.href) == "https://example.com/user/alice"
+    assert mention.name == "@alice"
+
+def test_invalid_link_missing_href():
+    """Test that Link creation fails when href is missing."""
+    with pytest.raises(ValidationError):
+        APLink(
+            id="https://example.com/link/123",
+            type="Link"
+        )
+
+def test_invalid_link_invalid_url():
+    """Test that Link creation fails with invalid URLs."""
+    with pytest.raises(ValidationError):
+        APLink(
+            id="https://example.com/link/123",
+            type="Link",
+            href="not-a-url"
+        )
+
+def test_invalid_link_invalid_media_type():
+    """Test that Link creation fails with invalid media type."""
+    with pytest.raises(ValidationError):
+        APLink(
+            id="https://example.com/link/123",
+            type="Link",
+            href="https://example.com/resource",
+            media_type="invalid/type"
+        )
+
+def test_invalid_mention_wrong_type():
+    """Test that Mention creation fails with wrong type."""
+    with pytest.raises(ValidationError):
+        APMention(
+            id="https://example.com/mention/123",
+            type="Link",  # Should be "Mention"
+            href="https://example.com/user/alice"
+        )
diff --git a/tests/tests_models/test_objects.py b/tests/tests_models/test_objects.py
new file mode 100644
index 0000000000000000000000000000000000000000..55d3a06422e0de1dfa1f1338d318af40209b68a1
--- /dev/null
+++ b/tests/tests_models/test_objects.py
@@ -0,0 +1,258 @@
+"""
+test_models.py
+This module contains tests for the ActivityPub models.
+"""
+import pytest
+from datetime import datetime, timedelta
+from pydantic import ValidationError
+
+from pyfed.models import (
+    APObject, APEvent, APPlace, APProfile, APRelationship, APTombstone,
+    APArticle, APAudio, APDocument, APImage, APNote, APPage, APVideo
+)
+
+def test_valid_ap_object():
+    ap_object = APObject(
+        id="https://example.com/object/123",
+        type="Object",
+        content="Sample content"
+    )
+    assert str(ap_object.id) == "https://example.com/object/123"
+    assert ap_object.type == "Object"
+    assert ap_object.content == "Sample content"
+
+def test_invalid_ap_object_missing_type():
+    with pytest.raises(ValidationError):
+        APObject(id="123", content="Sample content")
+
+def test_ap_object_with_optional_fields():
+    ap_object = APObject(
+        id="https://example.com/object/123",
+        type="Object",
+        content="Sample content",
+        name="Test Object",
+        published=datetime.now().isoformat(),
+        updated=datetime.now().isoformat(),
+        to=["https://example.com/user/1", "https://example.com/user/2"]
+    )
+    assert ap_object.name is not None
+    assert ap_object.published is not None
+    assert ap_object.updated is not None
+    assert isinstance(ap_object.to, list)
+
+def test_ap_object_is_public():
+    public_object = APObject(
+        id="https://example.com/object/123",
+        type="Object",
+        to=["https://www.w3.org/ns/activitystreams#Public"]
+    )
+    assert public_object.is_public()
+
+    private_object = APObject(
+        id="https://example.com/object/124",
+        type="Object",
+        to=["https://example.com/user/1"]
+    )
+    assert not private_object.is_public()
+
+@pytest.mark.parametrize("content", [
+    "",
+    "a" * 5001,
+    "<p>This is <strong>HTML</strong> content</p>",
+])
+def test_ap_object_content_edge_cases(content):
+    ap_object = APObject(
+        id="https://example.com/object/123",
+        type="Object",
+        content=content
+    )
+    assert ap_object.content == content
+
+def test_ap_object_get_mentions():
+    ap_object = APObject(
+        id="https://example.com/object/123",
+        type="Object",
+        tag=[
+            {"type": "Mention", "href": "https://example.com/user/1"},
+            {"type": "Mention", "href": "https://example.com/user/2"},
+            {"type": "Hashtag", "href": "https://example.com/tag/test"}
+        ]
+    )
+    mentions = ap_object.get_mentions()
+    assert len(mentions) == 2
+    assert "https://example.com/user/1" in mentions
+    assert "https://example.com/user/2" in mentions
+
+def test_valid_ap_event():
+    ap_event = APEvent(
+        id="https://example.com/event/123",
+        content="Event content",
+        start_time=datetime.now().isoformat(),
+        end_time=(datetime.now() + timedelta(hours=2)).isoformat()
+    )
+    assert ap_event.type == "Event"
+
+def test_ap_event_invalid_end_time():
+    with pytest.raises(ValidationError):
+        APEvent(
+            id="https://example.com/event/123",
+            content="Event content",
+            start_time="2024-01-01T10:00:00Z",
+            end_time="2024-01-01T09:00:00Z"
+        )
+
+def test_ap_event_with_place():
+    place = APPlace(
+        id="https://example.com/place/123",
+        name="Event Venue",
+        latitude=52.5200,
+        longitude=13.4050
+    )
+    event = APEvent(
+        id="https://example.com/event/123",
+        name="Test Event",
+        location=place
+    )
+    assert str(event.location.id) == "https://example.com/place/123"
+    assert event.location.name == "Event Venue"
+
+def test_valid_ap_place():
+    ap_place = APPlace(
+        id="https://example.com/place/123",
+        name="Test Place",
+        latitude=52.5200,
+        longitude=13.4050,
+        units="km"
+    )
+    assert ap_place.type == "Place"
+    assert ap_place.latitude == 52.5200
+    assert ap_place.longitude == 13.4050
+
+def test_ap_place_invalid_units():
+    with pytest.raises(ValidationError):
+        APPlace(
+            id="https://example.com/place/123",
+            name="Test Place",
+            latitude=52.5200,
+            longitude=13.4050,
+            units="invalid_unit"
+        )
+
+def test_ap_place_invalid_latitude():
+    with pytest.raises(ValidationError):
+        APPlace(
+            id="https://example.com/place/123",
+            name="Test Place",
+            latitude=91,  # Invalid latitude
+            longitude=13.4050
+        )
+
+def test_valid_ap_profile():
+    ap_profile = APProfile(
+        id="https://example.com/profile/123",
+        describes="https://example.com/user/123",
+        name="John Doe"
+    )
+    assert ap_profile.type == "Profile"
+    assert ap_profile.describes == "https://example.com/user/123"
+
+def test_valid_ap_relationship():
+    ap_relationship = APRelationship(
+        id="https://example.com/relationship/123",
+        subject="https://example.com/user/1",
+        object="https://example.com/user/2",
+        relationship="friend"
+    )
+    assert ap_relationship.type == "Relationship"
+    assert ap_relationship.relationship == "friend"
+
+def test_valid_ap_tombstone():
+    ap_tombstone = APTombstone(
+        id="https://example.com/tombstone/123",
+        former_type="Article",
+        deleted=datetime.now().isoformat()
+    )
+    assert ap_tombstone.type == "Tombstone"
+    assert ap_tombstone.former_type == "Article"
+
+def test_valid_ap_article():
+    ap_article = APArticle(
+        id="https://example.com/article/123",
+        name="Test Article",
+        content="This is a test article content."
+    )
+    assert ap_article.type == "Article"
+
+def test_valid_ap_audio():
+    ap_audio = APAudio(
+        id="https://example.com/audio/123",
+        name="Test Audio",
+        url="https://example.com/audio/123.mp3",
+        duration="PT2M30S"
+    )
+    assert ap_audio.type == "Audio"
+
+def test_valid_ap_document():
+    ap_document = APDocument(
+        id="https://example.com/document/123",
+        name="Test Document",
+        url="https://example.com/document/123.pdf"
+    )
+    assert ap_document.type == "Document"
+
+def test_valid_ap_image():
+    ap_image = APImage(
+        id="https://example.com/image/123",
+        name="Test Image",
+        url="https://example.com/image/123.jpg",
+        width=1920,
+        height=1080
+    )
+    assert ap_image.type == "Image"
+    assert ap_image.width == 1920
+    assert ap_image.height == 1080
+
+def test_ap_image_invalid_dimensions():
+    with pytest.raises(ValidationError):
+        APImage(
+            id="https://example.com/image/123",
+            name="Test Image",
+            url="https://example.com/image/123.jpg",
+            width=-1,
+            height=1080
+        )
+
+def test_valid_ap_note():
+    ap_note = APNote(
+        id="https://example.com/note/123",
+        content="This is a test note."
+    )
+    assert ap_note.type == "Note"
+
+def test_valid_ap_page():
+    ap_page = APPage(
+        id="https://example.com/page/123",
+        name="Test Page",
+        content="This is a test page content."
+    )
+    assert ap_page.type == "Page"
+
+def test_valid_ap_video():
+    ap_video = APVideo(
+        id="https://example.com/video/123",
+        name="Test Video",
+        url="https://example.com/video/123.mp4",
+        duration="PT1H30M",
+        media_type="video/mp4"
+    )
+    assert ap_video.type == "Video"
+
+def test_ap_video_invalid_media_type():
+    with pytest.raises(ValidationError):
+        APVideo(
+            id="https://example.com/video/123",
+            name="Test Video",
+            url="https://example.com/video/123.mp4",
+            duration="PT1H30M",
+            media_type="image/jpg"  # Invalid media type for video
+        )
diff --git a/tests/tests_serializers/__init__.py b/tests/tests_serializers/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/tests/tests_serializers/test_serialization.py b/tests/tests_serializers/test_serialization.py
new file mode 100644
index 0000000000000000000000000000000000000000..5f9468fe2a7c1d2e590da55a80645d1c82812951
--- /dev/null
+++ b/tests/tests_serializers/test_serialization.py
@@ -0,0 +1,190 @@
+"""
+test_serialization.py
+This module contains tests for JSON serialization of ActivityPub objects.
+"""
+import pytest
+from datetime import datetime
+from pydantic import ValidationError
+from pyfed.models import (
+    APObject, APPerson, APNote, APImage, APCollection,
+    APCreate, APLike, APFollow, APPlace, APEvent
+)
+from pyfed.serializers.json_serializer import ActivityPubSerializer
+
+def test_serialize_ap_object():
+    """Test basic object serialization."""
+    obj = APObject(
+        id="https://example.com/object/123",
+        type="Object",
+        name="Test Object",
+        content="This is a test object."
+    )
+    serialized = ActivityPubSerializer.serialize(obj)
+    assert '"@context"' in serialized
+    assert '"type": "Object"' in serialized
+    assert '"name": "Test Object"' in serialized
+
+def test_serialize_without_context():
+    """Test serialization without @context field."""
+    obj = APObject(
+        id="https://example.com/object/123",
+        type="Object",
+        name="Test Object"
+    )
+    serialized = ActivityPubSerializer.serialize(obj, include_context=False)
+    assert '"@context"' not in serialized
+
+def test_serialize_with_datetime():
+    """Test serialization of objects with datetime fields."""
+    now = datetime.utcnow()
+    obj = APObject(
+        id="https://example.com/object/123",
+        type="Object",
+        published=now,
+        updated=now
+    )
+    serialized = ActivityPubSerializer.serialize(obj)
+    assert now.isoformat() in serialized
+
+def test_serialize_nested_objects():
+    """Test serialization of objects with nested objects."""
+    author = APPerson(
+        id="https://example.com/users/alice",
+        type="Person",
+        name="Alice",
+        inbox="https://example.com/users/alice/inbox",
+        outbox="https://example.com/users/alice/outbox"
+    )
+    note = APNote(
+        id="https://example.com/notes/123",
+        type="Note",
+        content="Hello, World!",
+        attributed_to=author
+    )
+    serialized = ActivityPubSerializer.serialize(note)
+    assert '"attributedTo"' in serialized
+    assert '"type": "Person"' in serialized
+
+def test_serialize_collection():
+    """Test serialization of collections."""
+    items = [
+        APNote(
+            id=f"https://example.com/notes/{i}",
+            type="Note",
+            content=f"Note {i}"
+        ) for i in range(3)
+    ]
+    collection = APCollection(
+        id="https://example.com/collection/1",
+        type="Collection",
+        total_items=len(items),
+        items=items
+    )
+    serialized = ActivityPubSerializer.serialize(collection)
+    assert '"type": "Collection"' in serialized
+    assert '"totalItems": 3' in serialized
+    assert '"items"' in serialized
+
+def test_serialize_activity():
+    """Test serialization of activities."""
+    note = APNote(
+        id="https://example.com/notes/123",
+        type="Note",
+        content="Hello, World!"
+    )
+    create = APCreate(
+        id="https://example.com/activities/1",
+        type="Create",
+        actor="https://example.com/users/alice",
+        object=note
+    )
+    serialized = ActivityPubSerializer.serialize(create)
+    assert '"type": "Create"' in serialized
+    assert '"object"' in serialized
+    assert '"content": "Hello, World!"' in serialized
+
+def test_serialize_with_urls():
+    """Test serialization of objects with URL fields."""
+    place = APPlace(
+        id="https://example.com/places/123",
+        type="Place",
+        name="Test Place",
+        latitude=51.5074,
+        longitude=-0.1278
+    )
+    event = APEvent(
+        id="https://example.com/events/123",
+        type="Event",
+        name="Test Event",
+        location=place,
+        url="https://example.com/events/123/details"
+    )
+    serialized = ActivityPubSerializer.serialize(event)
+    assert '"url":' in serialized
+    assert '"location"' in serialized
+
+def test_deserialize_ap_object():
+    """Test basic object deserialization."""
+    json_str = '''
+    {
+        "@context": "https://www.w3.org/ns/activitystreams",
+        "type": "Object",
+        "id": "https://example.com/object/123",
+        "name": "Test Object",
+        "content": "This is a test object."
+    }
+    '''
+    obj = ActivityPubSerializer.deserialize(json_str, APObject)
+    assert obj.type == "Object"
+    assert obj.name == "Test Object"
+    assert str(obj.id) == "https://example.com/object/123"
+
+def test_deserialize_invalid_json():
+    """Test deserialization of invalid JSON."""
+    with pytest.raises(ValueError):
+        ActivityPubSerializer.deserialize("invalid json", APObject)
+
+def test_deserialize_missing_required_fields():
+    """Test deserialization with missing required fields."""
+    json_str = '{"type": "Object", "name": "Test"}'  # Missing required 'id'
+    with pytest.raises(ValidationError):
+        ActivityPubSerializer.deserialize(json_str, APObject)
+
+def test_serialize_deserialize_complex_object():
+    """Test round-trip serialization and deserialization."""
+    original = APNote(
+        id="https://example.com/notes/123",
+        type="Note",
+        content="Test content",
+        to=["https://example.com/users/bob"],
+        cc=["https://www.w3.org/ns/activitystreams#Public"]
+    )
+    serialized = ActivityPubSerializer.serialize(original)
+    deserialized = ActivityPubSerializer.deserialize(serialized, APNote)
+    assert deserialized.id == original.id
+    assert deserialized.content == original.content
+    assert deserialized.to == original.to
+    assert deserialized.cc == original.cc
+
+def test_serialize_with_custom_json_options():
+    """Test serialization with custom JSON options."""
+    obj = APObject(
+        id="https://example.com/object/123",
+        type="Object",
+        name="Test Object"
+    )
+    serialized = ActivityPubSerializer.serialize(obj, indent=2)
+    assert '\n  "' in serialized  # Check for indentation
+
+def test_deserialize_with_extra_fields():
+    """Test deserialization with extra fields in JSON."""
+    json_str = '''
+    {
+        "type": "Object",
+        "id": "https://example.com/object/123",
+        "name": "Test Object",
+        "extra_field": "Should be ignored"
+    }
+    '''
+    obj = ActivityPubSerializer.deserialize(json_str, APObject)
+    assert not hasattr(obj, "extra_field")