Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 150 additions & 8 deletions backend/apps/api/rest/v0/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,34 @@
class ProjectBase(Schema):
"""Base schema for Project (used in list endpoints)."""

created_at: datetime
key: str
level: ProjectLevel
name: str
updated_at: datetime
created_at: datetime = Field(
...,
description="Project creation timestamp (ISO 8601).",
example="2019-09-12T20:15:45Z",
)
key: str = Field(
...,
description=(
"Stable project key used as the identifier in API URLs. "
"Use this value as `{project_id}` in `GET /api/v0/projects/{project_id}`."
),
example="cheat-sheets",
)
level: ProjectLevel = Field(
...,
description="Project maturity level.",
example="flagship",
)
name: str = Field(
...,
description="Human-readable project name.",
example="OWASP Cheat Sheet Series",
)
updated_at: datetime = Field(
...,
description="Last updated timestamp (ISO 8601).",
example="2025-12-15T15:12:05Z",
)
Comment on lines +24 to +51
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the file exists and examine the code
if [ -f "backend/apps/api/rest/v0/project.py" ]; then
  echo "=== File exists ===" 
  wc -l backend/apps/api/rest/v0/project.py
  echo ""
  echo "=== Lines 24-51 ===" 
  sed -n '24,51p' backend/apps/api/rest/v0/project.py
  echo ""
  echo "=== Full file structure ===" 
  head -60 backend/apps/api/rest/v0/project.py
else
  echo "File not found"
fi

Repository: OWASP/Nest

Length of output: 2723


🏁 Script executed:

# Check for version requirements
find . -type f \( -name "pyproject.toml" -o -name "requirements.txt" -o -name "requirements*.txt" -o -name "setup.py" \) | head -5 | while read f; do
  echo "=== $f ==="
  cat "$f" | grep -i -E "(pydantic|django-ninja|django_ninja)" || true
done

Repository: OWASP/Nest

Length of output: 244


🌐 Web query:

Pydantic v2 Field example parameter deprecated vs examples json_schema_extra

💡 Result:

Short answer:

  • Pydantic v2 does not use a Field(..., example=...) singular parameter. Use Field(..., examples=[...]) (the documented field-level parameter) or Field(..., json_schema_extra={...}) to add example(s) or other arbitrary JSON Schema keys. [1][2]

  • For deprecation: Field(..., deprecated=...) is supported in v2 — it emits a runtime DeprecationWarning when accessed and adds "deprecated": true to the generated JSON Schema (you may pass True, a string message, or a warnings.deprecated decorator). Use this to mark fields as deprecated in both runtime behavior and the JSON schema. [3][2]

References:

  • json_schema_extra / examples docs. [1]
  • Field parameters (examples, json_schema_extra) and JSON Schema customization. [2]
  • deprecated field behavior and JSON Schema deprecation. [3]

🌐 Web query:

django-ninja 1.4.3 Pydantic v2 Field example parameter OpenAPI support

💡 Result:

Short answer: Yes — django-ninja v1.4.x is Pydantic‑v2‑aware, and OpenAPI examples come from Pydantic v2 field JSON‑schema generation. Use Pydantic v2 Field parameters (examples or json_schema_extra) to supply OpenAPI examples; django-ninja will include whatever Pydantic outputs in the generated schema.

How to do it (example):

  • Preferred: use Field(..., examples=[...]) or Field(..., json_schema_extra={"example": ...}) on your Pydantic/Scheme fields. Django‑Ninja uses Pydantic’s JSON Schema for OpenAPI, so those values appear in /openapi.json and the docs UI. See Pydantic docs on field JSON‑schema customization. [1][2]

References:

  • Django‑Ninja releases / v1.x Pydantic v2 migration notes. [3]
  • Django‑Ninja Schema guide (shows Field usage in Schema). [4]
  • Pydantic JSON Schema / Field customization (examples, json_schema_extra). [1]

If you want, I can show a minimal code snippet demonstrating Field(..., examples=[...]) and the resulting openapi fragment.


Use Pydantic v2-compatible examples or json_schema_extra instead of deprecated example parameter.

The example parameter in Field() is not supported in Pydantic v2. Since django-ninja generates OpenAPI schemas from Pydantic's JSON schema output, the examples will not appear in the OpenAPI documentation. Use either examples=[...] (list format) or json_schema_extra={"example": ...} instead:

created_at: datetime = Field(
    ...,
    description="Project creation timestamp (ISO 8601).",
    examples=["2019-09-12T20:15:45Z"],
)

Or:

created_at: datetime = Field(
    ...,
    description="Project creation timestamp (ISO 8601).",
    json_schema_extra={"example": "2019-09-12T20:15:45Z"},
)

Apply this fix to all Field definitions in the ProjectBase schema (lines 24-51).

🤖 Prompt for AI Agents
In `@backend/apps/api/rest/v0/project.py` around lines 24 - 51, Replace deprecated
Pydantic v1 Field(example=...) usages in the ProjectBase schema with a
v2-compatible format: update each Field for created_at, key, level, name, and
updated_at to use either examples=[...] (e.g.,
examples=["2019-09-12T20:15:45Z"]) or json_schema_extra={"example": ...} instead
of example=..., keeping the existing descriptions and types (ProjectLevel,
datetime, str) and preserving the Field(...) structure.


@staticmethod
def resolve_key(obj: ProjectModel) -> str:
Expand Down Expand Up @@ -63,24 +86,143 @@ class ProjectFilter(FilterSchema):

level: ProjectLevel | None = Field(
None,
description="Level of the project",
description=(
"Filter by project level.\n\n"
"Must be one of: `other`, `incubator`, `lab`, `production`, `flagship`."
),
example="flagship",
)


@router.get(
"/",
description="Retrieve a paginated list of OWASP projects.",
description=(
"Retrieve a paginated list of OWASP projects.\n\n"
"Use this endpoint to discover project keys/IDs that can be used with "
"`GET /api/v0/projects/{project_id}`.\n\n"
"### Authentication\n"
"In non-local environments this endpoint requires an API key in the `X-API-Key` header.\n"
"### Pagination\n"
"Pagination query parameters are provided by the API's configured Django Ninja pagination "
"class "
"(see the query params shown below in Swagger).\n\n"
"Common patterns are either:\n"
"- `page` and `page_size` (page-number pagination)\n"
"- `limit` and `offset` (limit-offset pagination)\n\n"
"### Validation errors\n"
"Invalid `level`, `ordering`, or pagination values may return `422 Unprocessable Content`."
),
operation_id="list_projects",
response=list[Project],
summary="List projects",
openapi_extra={
"responses": {
401: {
"description": (
"Unauthorized — missing or invalid API key (non-local environments)."
),
"content": {
"application/json": {
"examples": {
"Missing or invalid API key": {
"value": {"message": "Missing or invalid API key"}
}
}
}
},
},
404: {
"description": "Not Found — page out of range (when using page-based pagination).",
"content": {
"application/json": {
"examples": {
"Page out of range": {
"value": {
"detail": (
"Not Found: Page 3000 not found. Valid pages are 1 to 3."
)
}
}
}
}
},
},
422: {
"description": (
"Unprocessable Content — invalid query parameters "
"(e.g., invalid level, ordering, or page)."
),
"content": {
"application/json": {
"examples": {
"Invalid level + ordering": {
"value": {
"detail": [
{
"type": "enum",
"loc": ["query", "level"],
"msg": (
"Input should be 'other', "
"'incubator', 'lab', "
"'production' or 'flagship'"
),
"ctx": {
"expected": (
"'other', 'incubator', "
"'lab', 'production' "
"or 'flagship'"
)
},
},
{
"type": "literal_error",
"loc": ["query", "ordering"],
"msg": (
"Input should be 'created_at', "
"'-created_at', 'updated_at' "
"or '-updated_at'"
),
"ctx": {
"expected": (
"'created_at', '-created_at', "
"'updated_at' or '-updated_at'"
)
},
},
]
}
},
"Invalid page (page=0)": {
"value": {
"detail": [
{
"type": "greater_than_equal",
"loc": ["query", "page"],
"msg": "Input should be greater than or equal to 1",
"ctx": {"ge": 1},
}
]
}
},
}
}
},
},
}
},
)
@decorate_view(cache_response())
def list_projects(
request: HttpRequest,
filters: ProjectFilter = Query(...),
ordering: Literal["created_at", "-created_at", "updated_at", "-updated_at"] | None = Query(
None,
description="Ordering field",
description=(
"Sort order for results.\n\n"
"Allowed values: `created_at`, `-created_at`, `updated_at`, `-updated_at`.\n"
"If not provided, the API applies a default ordering."
),
example="-updated_at",
),
) -> list[Project]:
"""Get projects."""
Expand Down
205 changes: 205 additions & 0 deletions docs/api/projects.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
# GET /api/v0/projects/ – List projects

## Overview

Retrieve a paginated list of OWASP projects.

Use this endpoint to obtain project metadata and to discover project keys/IDs that can be used with `GET /api/v0/projects/{project_id}`.

---

## Authentication

In non-local environments (staging and production), this endpoint is protected by an API key:

- The API uses a custom `ApiKey` authentication class that extends `APIKeyHeader`.
- Clients must send a valid API key in the HTTP header:

X-API-Key: YOUR_API_KEY_VALUE

- If the `X-API-Key` header is missing or invalid, the API returns `401 Unauthorized` with a message indicating a missing or invalid API key.
- Authenticated requests are subject to an auth-based rate limit of **10 requests per second**.

In local development (`IS_LOCAL_ENVIRONMENT`):

- auth is disabled and this endpoint is publicly accessible without authentication.
- throttling is disabled.

---

## Request

Method: GET
Path: /api/v0/projects/

### Query parameters

- level (string, query, optional)
Level of the project. Must be one of:
- other
- incubator
- lab
- production
- flagship

- ordering (string, query, optional)
Ordering field. Must be one of:
- created_at
- -created_at
- updated_at
- -updated_at

If ordering is not provided, the API applies a default ordering in code.

- page (integer, query, optional, default: 1)
Page number (must be >= 1). If page is less than 1 (for example page=0), the API returns 422 Unprocessable Content.

- page_size (integer, query, optional, default: 100)
Number of items per page.

### Example request (from OpenAPI “Try it out”, local dev)

curl -X 'GET' \
'http://localhost:8000/api/v0/projects/?level=flagship&ordering=created_at&page=1&page_size=100' \
-H 'accept: application/json'

In a non-local environment, you would also include the API key header:

curl -X 'GET' \
'https://<nest-base-url>/api/v0/projects/?level=flagship&ordering=created_at&page=1&page_size=100' \
-H 'accept: application/json' \
-H 'X-API-Key: YOUR_API_KEY_VALUE'

---

## Response

### 200 OK – Paginated project list

Media type: application/json

#### Example response (truncated for brevity)

Note: The items array below is intentionally truncated to keep the example readable.

{
"current_page": 1,
"has_next": true,
"has_previous": false,
"total_count": 252,
"total_pages": 3,
"items": [
{
"created_at": "2019-09-12T20:15:45Z",
"key": "cheat-sheets",
"level": "flagship",
"name": "OWASP Cheat Sheet Series",
"updated_at": "2025-12-15T15:12:05Z"
},
{
"created_at": "2019-09-12T20:24:13Z",
"key": "mobile-app-security",
"level": "flagship",
"name": "OWASP Mobile Application Security",
"updated_at": "2025-09-18T06:50:45Z"
}
]
}

Field overview (based on the schema):

- current_page
- has_next
- has_previous
- items[] (array of project objects when present):
- created_at
- key
- level
- name
- updated_at
- total_count
- total_pages

---

## Notes

### Pre-sync local development behavior (no projects exist)

In a fresh local development environment (before syncing data), the API may return an empty items array.

Example response (pre-sync):

{
"current_page": 1,
"has_next": false,
"has_previous": false,
"items": [],
"total_count": 0,
"total_pages": 1
}

---

### 404 Not Found — Page out of range

Happens when page is greater than total_pages

{
"detail": "Not Found: Page 3000 not found. Valid pages are 1 to 3."
}

---

### 422 Unprocessable Content – Invalid query parameters

Returned when query parameters do not match the expected values. For example, sending level=1 or ordering=1 results in a response similar to:

{
"detail": [
{
"type": "enum",
"loc": [
"query",
"level"
],
"msg": "Input should be 'other', 'incubator', 'lab', 'production' or 'flagship'",
"ctx": {
"expected": "'other', 'incubator', 'lab', 'production' or 'flagship'"
}
},
{
"type": "literal_error",
"loc": [
"query",
"ordering"
],
"msg": "Input should be 'created_at', '-created_at', 'updated_at' or '-updated_at'",
"ctx": {
"expected": "'created_at', '-created_at', 'updated_at' or '-updated_at'"
}
}
]
}

To fix, update level and ordering to one of the valid values listed above.

#### Example – Invalid page (e.g., page=0)

{
"detail": [
{
"type": "greater_than_equal",
"loc": [
"query",
"page"
],
"msg": "Input should be greater than or equal to 1",
"ctx": {
"ge": 1
}
}
]
}

To fix ensure page is >= 1.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor grammar fix: missing comma.

📝 Suggested fix
-To fix ensure page is >= 1.
+To fix, ensure page is >= 1.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
To fix ensure page is >= 1.
To fix, ensure page is >= 1.
🤖 Prompt for AI Agents
In `@docs/api/projects.md` at line 205, Update the sentence "To fix ensure page is
>= 1." in docs (the API projects documentation) to include the missing comma so
it reads "To fix, ensure page is >= 1." — locate the exact string "To fix ensure
page is >= 1." and add the comma after "fix" to correct the grammar.