Repository: openfoodfacts/openfoodfacts-server
Pull request #11847: feat: Added Spectral as tool for Linting in OpenApi Spec file 
Author: login: JagjeevanAK
Labels: name: 📍🏭 Packager codes
description: https://blog.openfoodfacts.org/en/news/discover-what-food-products-are-made-near-you-with-made-near-
name: 📚 Documentation
description: Documentation issues improve the project for everyone.
name: 💥 Merge Conflicts
description: 💥 Merge Conflicts
name: GitHub Actions
description: Pull requests that update Github_actions code
name: API v3
name: 📚 OpenAPI
Created: 2025-05-01T13:51:40Z | Merged: 2025-05-26T14:10:07Z

Signed-off-by: Jagjeevan <83103469+JagjeevanAK@users.noreply.github.com>
### What
1. added `.spectral.yaml` file to write the rules for linting of Spec-file.
2. Added `Github action` for to automate the process.
3. Current issues in OpenAPI Specfile
```yml
# For api.yml file

➜  openfoodfacts-server git:(JagjeevanAK/SpecFile) ✗ spectral lint /Users/jagjeevankashid/Developer/open-source/my_copies/openfoodfacts-server/docs/api/ref/api.yml --ruleset ./.spectral.yaml 

/Users/jagjeevankashid/Developer/open-source/my_copies/openfoodfacts-server/docs/api/ref/api.yml
 183:10  warning  operation-tags         Operation must have non-empty "tags" array.                  paths./cgi/product_image_unselect.pl.post
 361:22    error  array-items            Schemas with "type: array", require a sibling "items" field  paths./cgi/suggest.pl.get.responses[200].content.application/json.schema
 404:11  warning  operation-tag-defined  Operation tags must be defined in global tags.               paths./api/v2/attribute_groups.get.tags[0]
 427:11  warning  operation-tag-defined  Operation tags must be defined in global tags.               paths./api/v2/preferences.get.tags[0]
 494:20  warning  oas3-unused-component  Potentially unused component has been detected.              components.schemas.AppCredentials
 540:13  warning  oas3-unused-component  Potentially unused component has been detected.              components.schemas.Product
 542:16  warning  oas3-unused-component  Potentially unused component has been detected.              components.schemas.Ingredient
 545:14  warning  oas3-unused-component  Potentially unused component has been detected.              components.schemas.Nutrient
 656:31  warning  oas3-unused-component  Potentially unused component has been detected.              components.parameters.knowledge_panels_included
 664:31  warning  oas3-unused-component  Potentially unused component has been detected.              components.parameters.knowledge_panels_excluded

/Users/jagjeevankashid/Developer/open-source/my_copies/openfoodfacts-server/docs/api/ref/schemas/product_meta.yaml
 11:14  error  oas3-valid-schema-example  "example" property type must be integer  properties.created_t.example

✖ 11 problems (2 errors, 9 warnings, 0 infos, 0 hints)

# For api-v3.yml file

➜  openfoodfacts-server git:(JagjeevanAK/SpecFile) ✗ spectral lint /Users/jagjeevankashid/Developer/open-source/my_copies/openfoodfacts-server/docs/api/ref/api-v3.yml --ruleset ./.spectral.yaml

/Users/jagjeevankashid/Developer/open-source/my_copies/openfoodfacts-server/docs/api/ref/api-v3.yml
 117:11  warning  operation-tags         Operation must have non-empty "tags" array.      paths./api/v3/product/{barcode}.patch
 261:12  warning  operation-tags         Operation must have non-empty "tags" array.      paths./api/v3/taxonomy_suggestions.get.tags
 357:12  warning  operation-tags         Operation must have non-empty "tags" array.      paths./api/v3/tag/{tagtype}/{tag_or_tagid}.get.tags
 404:12  warning  operation-tags         Operation must have non-empty "tags" array.      paths./api/v3/product_revert.post.tags
 451:10  warning  oas3-unused-component  Potentially unused component has been detected.  components.parameters.code

/Users/jagjeevankashid/Developer/open-source/my_copies/openfoodfacts-server/docs/api/ref/schemas/product_meta.yaml
 11:14  error  oas3-valid-schema-example  "example" property type must be integer  properties.created_t.example

✖ 6 problems (1 error, 5 warnings, 0 infos, 0 hints)
```

4. Solved some current issues in Spec-file
<!-- Describe the changes made and why they were made instead of how they were made. -->


Diff:
diff --git a/.DS_Store b/.DS_Store
deleted file mode 100644
index 2243e75e7bc6c..0000000000000
Binary files a/.DS_Store and /dev/null differ
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 0e98abd28d3aa..8d5feb1871f7b 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -255,8 +255,8 @@ Footer:
 - changed-files:
   # core
   - any-glob-to-any-file: 'docs/assets/api-rapidoc.html'
-  - any-glob-to-any-file: 'docs/api/ref/api.yml'
-  - any-glob-to-any-file: 'docs/api/ref/api-v3.yml'
+  - any-glob-to-any-file: 'docs/api/ref/api.yaml'
+  - any-glob-to-any-file: 'docs/api/ref/api-v3.yaml'
   - any-glob-to-any-file: 'docs/​api/​ref/​schemas/​tags_parameters.yaml'
   # product core
   - any-glob-to-any-file: 'docs/api/ref/schemas/product_base.yaml'
@@ -1489,7 +1489,7 @@ API:
 # https://openfoodfacts.github.io/openfoodfacts-server/api/
 API v3:
 - changed-files:
-  - any-glob-to-any-file: 'docs/api/ref/api-v3.yml'
+  - any-glob-to-any-file: 'docs/api/ref/api-v3.yaml'
   - any-glob-to-any-file: 'tests/integration/api_v3_product_read.t'
   - any-glob-to-any-file: 'tests/integration/api_v3_product_write.t'
 
diff --git a/.github/workflows/api-linting.yml b/.github/workflows/api-linting.yml
new file mode 100644
index 0000000000000..09100cf2e9d36
--- /dev/null
+++ b/.github/workflows/api-linting.yml
@@ -0,0 +1,31 @@
+name: OpenAPI Spec Linting
+
+on:
+  pull_request:
+    paths:
+      - 'docs/api/ref/**/*.yaml'
+  push:
+    branches:
+      - main
+    paths:
+      - 'docs/api/ref/**/*.yaml'
+
+jobs:
+  lint-api:
+    name: Lint OpenAPI Spec Files
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+
+      - name: Setup Node.js
+        uses: actions/setup-node@v4
+        with:
+          node-version: '22'
+
+      - name: Run Spectral lint
+        uses: stoplightio/spectral-action@latest
+        with:
+          file_glob: 'docs/api/ref/*.yaml'
+          spectral_ruleset: .spectral.yaml
diff --git a/.github/workflows/generate-doc.yml b/.github/workflows/generate-doc.yml
index 8a2c92aead725..61ad167fe09e2 100644
--- a/.github/workflows/generate-doc.yml
+++ b/.github/workflows/generate-doc.yml
@@ -52,7 +52,7 @@ jobs:
       #     docker run --rm \
       #       -v $(pwd)/docs/api/ref:/data -v $(pwd)/gh_pages/:/output \
       #       ghcr.io/redocly/redoc/cli:latest \
-      #       build -o /output/api/ref-v2/index.html api.yml && \
+      #       build -o /output/api/ref-v2/index.html api.yaml && \
       #     sudo chown $UID -R gh_pages
 
       # - name: Generate openapi html for v3 with ghcr.io/redocly/redoc/cli:latest
@@ -60,7 +60,7 @@ jobs:
       #     docker run --rm \
       #       -v $(pwd)/docs/api/ref:/data -v $(pwd)/gh_pages/:/output \
       #       ghcr.io/redocly/redoc/cli:latest \
-      #       build -o /output/api/ref-v3/index.html api-v3.yml && \
+      #       build -o /output/api/ref-v3/index.html api-v3.yaml && \
       #     sudo chown $UID -R gh_pages
 
       - name: Validate OpenAPI
@@ -76,7 +76,7 @@ jobs:
           cp docs/assets/api-rapidoc.html gh_pages/api/ref-v2/index.html
           # v3 api - change yaml and link to api-v3
           cat docs/assets/api-rapidoc.html | \
-            sed -e "s/api.yml/api-v3.yml/" -e 's|ref-v3/">API v3|ref-v2/">API v2|' \
+            sed -e "s/api.yaml/api-v3.yaml/" -e 's|ref-v3/">API v3|ref-v2/">API v2|' \
             > gh_pages/api/ref-v3/index.html
 
 
diff --git a/.spectral.yaml b/.spectral.yaml
new file mode 100644
index 0000000000000..ba6e3656baa4d
--- /dev/null
+++ b/.spectral.yaml
@@ -0,0 +1 @@
+extends: "spectral:oas"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f754ae6708495..584bbec8c165f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2070,7 +2070,7 @@
 ### Technical
 
 * Docs overhaul ([#8129](https://github.com/openfoodfacts/openfoodfacts-server/issues/8129)) ([649ed8d](https://github.com/openfoodfacts/openfoodfacts-server/commit/649ed8daf62ba23e34f1e0a7b67636aac679b630))
-* Make api.yml a valid OpenAPI-3.1.0-Spec ([#8042](https://github.com/openfoodfacts/openfoodfacts-server/issues/8042)) ([8a99b8f](https://github.com/openfoodfacts/openfoodfacts-server/commit/8a99b8fd77549b5b2fb20b17c76c1d754be2d661))
+* Make api.yaml a valid OpenAPI-3.1.0-Spec ([#8042](https://github.com/openfoodfacts/openfoodfacts-server/issues/8042)) ([8a99b8f](https://github.com/openfoodfacts/openfoodfacts-server/commit/8a99b8fd77549b5b2fb20b17c76c1d754be2d661))
 * small consistency fix in Test.pm ([#8116](https://github.com/openfoodfacts/openfoodfacts-server/issues/8116)) ([db6077e](https://github.com/openfoodfacts/openfoodfacts-server/commit/db6077e815e25ad29d0b9254d833d7f7ecf518b3))
 
 
diff --git a/Makefile b/Makefile
index d8b0063e0e3d3..16ba5930b0211 100644
--- a/Makefile
+++ b/Makefile
@@ -421,14 +421,24 @@ lint_taxonomies:
 check_openapi_v2:
 	docker run --rm \
 		-v ${PWD}:/local openapitools/openapi-generator-cli validate --recommend \
-		-i /local/docs/api/ref/api.yml
+		-i /local/docs/api/ref/api.yaml
 
 check_openapi_v3:
 	docker run --rm \
 		-v ${PWD}:/local openapitools/openapi-generator-cli validate --recommend \
-		-i /local/docs/api/ref/api-v3.yml
+		-i /local/docs/api/ref/api-v3.yaml
+
+check_openapi_spectral:
+# Currently, Spectral does not support Mac with m3 chip, for more details: https://github.com/stoplightio/spectral/issues/2636
+	@if [ "$(OS)" = "Darwin" ]; then \
+		echo "🥫 Linting OpenAPI is not supported on macOS"; \
+	else \
+		echo "🥫 Linting OpenAPI files"; \
+		docker run --rm -v $$(pwd):/app stoplight/spectral lint -r /app/.spectral.yaml /app/docs/api/ref/api.yaml /app/docs/api/ref/api-v3.yaml; \
+	fi
+
+check_openapi: check_openapi_v2 check_openapi_v3 check_openapi_spectral
 
-check_openapi: check_openapi_v2 check_openapi_v3
 
 #-------------#
 # Compilation #
diff --git a/README.md b/README.md
index 17b5b4e36dd3b..61d39806cbeb4 100644
--- a/README.md
+++ b/README.md
@@ -81,7 +81,7 @@ Visit the [website](https://world.openfoodfacts.org) for more info.
 - Join us on slack at <https://openfoodfacts.slack.com/> in the channels: `#api`, `#productopener`, `#dev`.
 - Open Food Facts API documentation:
   - [Introduction to the API](https://openfoodfacts.github.io/openfoodfacts-server/api/)
-  - [Full reference](https://openfoodfacts.github.io/openfoodfacts-server/api/ref-v2/) ([source](https://github.com/openfoodfacts/openfoodfacts-server/tree/main/docs/api/ref/api.yml))
+  - [Full reference](https://openfoodfacts.github.io/openfoodfacts-server/api/ref-v2/) ([source](https://github.com/openfoodfacts/openfoodfacts-server/tree/main/docs/api/ref/api.yaml))
 - Developer documentation:
   - To start coding, head to the [Quick start guide (docker)](./docs/dev/how-to-quick-start-guide.md)
   - Additional documentation
diff --git a/docs/api/ref-v2.md b/docs/api/ref-v2.md
index 2174302f423cf..a4626c10ad8b3 100644
--- a/docs/api/ref-v2.md
+++ b/docs/api/ref-v2.md
@@ -1,5 +1,5 @@
 # Reference OpenAPI documentation for v2
 
-See api.yml for edition.
+See api.yaml for edition.
 
-Do not write anything here, it is meant to be overwritten by html generated from api.yml
+Do not write anything here, it is meant to be overwritten by html generated from api.yaml
diff --git a/docs/api/ref-v3.md b/docs/api/ref-v3.md
index f95d97734df78..2c9d9428c513d 100644
--- a/docs/api/ref-v3.md
+++ b/docs/api/ref-v3.md
@@ -1,5 +1,5 @@
 # Reference OpenAPI documentation for v3
 
-See api-v3.yml for edition.
+See api-v3.yaml for edition.
 
-Do not write anything here, it is meant to be overwritten by html generated from api-v3.yml
+Do not write anything here, it is meant to be overwritten by html generated from api-v3.yaml
diff --git a/docs/api/ref/api-v3.yml b/docs/api/ref/api-v3.yaml
similarity index 98%
rename from docs/api/ref/api-v3.yml
rename to docs/api/ref/api-v3.yaml
index 5c379af20efd2..db86c59a51864 100644
--- a/docs/api/ref/api-v3.yml
+++ b/docs/api/ref/api-v3.yaml
@@ -117,6 +117,8 @@ paths:
     patch:
       summary: "WRITE Product - Create or update product, or analyze test product (API V3 - Implementation in progress)"
       operationId: patch-api-v3-product-barcode
+      tags:
+        - Write Requests
       responses:
         "200":
           description: |-
@@ -265,7 +267,8 @@ paths:
     parameters: []
     get:
       summary: Get taxonomy entries suggestions
-      tags: []
+      tags:
+        - Read Requests
       responses:
         "200":
           description: OK
@@ -306,7 +309,7 @@ paths:
 
         If a string is passed, an additional sort is done to put first suggestions that start with the string, followed by suggestions with a word that start with the string, and then suggestions that contain the string anywhere.
       parameters:
-        - $ref: ./api.yml#/components/parameters/tagtype
+        - $ref: ./api.yaml#/components/parameters/tagtype
         - $ref: "#/components/parameters/cc"
         - $ref: "#/components/parameters/lc"
         - schema:
@@ -361,7 +364,8 @@ paths:
         description: "Tag name (e.g. yogurts) or tag id (e.g. en:yogurts)"
     get:
       summary: Get knowledge panels for a tag
-      tags: []
+      tags:
+        - Read Requests
       responses:
         "200":
           description: OK
@@ -408,7 +412,8 @@ paths:
     parameters: []
     post:
       summary: Revert a product to a previous revision
-      tags: []
+      tags:
+        - Write Requests
       responses:
         "200":
           description: OK
@@ -455,14 +460,6 @@ components:
       name: lc
       required: false
       description: "2 letter code of the language of the user. Used for localizing some fields in returned values (e.g. knowledge panels). If not passed, the language may be inferred by the Accept-Language header of the request."
-    code:
-      schema:
-        type: string
-        example: "4251105501381"
-      in: query
-      name: code
-      description: Barcode of the product
-      required: true
 tags:
   - name: Read Requests
   - name: Write Requests
diff --git a/docs/api/ref/api.yml b/docs/api/ref/api.yaml
similarity index 96%
rename from docs/api/ref/api.yml
rename to docs/api/ref/api.yaml
index 432a465d697bc..caf45dcc5fd76 100644
--- a/docs/api/ref/api.yml
+++ b/docs/api/ref/api.yaml
@@ -53,6 +53,8 @@ paths:
               - "3017620422003"
         - $ref: "./parameters/requested_product_type.yaml#/components/parameters/RequestedProductType"
         - $ref: "#/components/parameters/fields"
+        - $ref: "#/components/parameters/knowledge_panels_included"
+        - $ref: "#/components/parameters/knowledge_panels_excluded"
       responses:
         "200":
           description: OK
@@ -107,7 +109,9 @@ paths:
         content:
           multipart/form-data:
             schema:
-              $ref: ./requestBodies/add_photo_to_existing_product.yaml
+              allOf:
+                - $ref: ./requestBodies/add_photo_to_existing_product.yaml
+                - $ref: ./requestBodies/change_ref_properties.yaml
         description: ""
       security:
         - cookieAuth: []
@@ -154,7 +158,9 @@ paths:
         content:
           multipart/form-data:
             schema:
-              $ref: ./requestBodies/crop_a_photo.yaml
+              allOf:
+                - $ref: ./requestBodies/crop_a_photo.yaml
+                - $ref: ./requestBodies/change_ref_properties.yaml
         required: true
       tags:
         - Write Requests
@@ -186,6 +192,8 @@ paths:
         This endpoint allows the user to unselect a photo for a product.
         The user must provide the product code and the image ID to unselect.
       operationId: post-cgi-product_image_unselect.pl
+      tags:
+        - Write Requests
       requestBody:
         content:
           multipart/form-data:
@@ -360,6 +368,8 @@ paths:
             application/json:
               schema:
                 type: array
+                items:
+                  type: string
       operationId: get-cgi-suggest.pl
       parameters:
         - $ref: "#/components/parameters/tagtype"
@@ -495,26 +505,6 @@ components:
       name: User-Agent
 
   schemas:
-    AppCredentials:
-      description: Schema for application credentials
-      type: object
-      properties:
-        app_name:
-          type: string
-          description: Name of the application
-        app_version:
-          type: string
-          description: Version of the application
-        app_uuid:
-          type: string
-          description: |
-            A salted random uuid (Unique identifier) for the user so that Open Food Facts moderators 
-            can selectively ban any problematic user without banning your whole app account.
-      required:
-        - app_name
-        - app_version
-        - app_uuid
-
     "Product-Base":
       $ref: ./schemas/product_base.yaml
     "Product-Misc":
@@ -541,6 +531,7 @@ components:
       $ref: ./schemas/product_knowledge_panels.yaml
     "Product-Attribute-Groups":
       $ref: "./schemas/product_attribute_groups.yaml"
+    # this is to have it displayed wholy in documentation
     Product:
       $ref: ./schemas/product.yaml
     Ingredient:
@@ -697,3 +688,5 @@ tags:
     description: Endpoints that allow writing or modifying data in the Open Food Facts database.
   - name: Authentication
     description: Endpoints used to retrieve authentication info.
+  - name: Personal search
+    description: Endpoints related to personal search and recommendation
diff --git a/docs/api/ref/schemas/ingredient.yaml b/docs/api/ref/schemas/ingredient.yaml
index 9b46c3b8690dc..290c00b1484ba 100644
--- a/docs/api/ref/schemas/ingredient.yaml
+++ b/docs/api/ref/schemas/ingredient.yaml
@@ -7,7 +7,7 @@ components:
         id:
           type: string
         ingredients:
-          $ref: "#/components/schemas/Ingredients"
+          $ref: "./ingredient.yaml#/components/schemas/Ingredients"
         percent:
           type: integer
           description: The percentage of the ingredient.
@@ -38,4 +38,4 @@ components:
         This structure gives the different ingredients and some information about them,
         like estimate on their quantity.
       items:
-        $ref: "#/components/schemas/Ingredient"
+        $ref: "./ingredient.yaml#/components/schemas/Ingredient"
diff --git a/docs/api/ref/schemas/product.yaml b/docs/api/ref/schemas/product.yaml
index 059749e3ec926..55f3aeae8b4c9 100644
--- a/docs/api/ref/schemas/product.yaml
+++ b/docs/api/ref/schemas/product.yaml
@@ -19,16 +19,16 @@ description: |
   * [Product Attribute Groups](#cmp--schemas-product-attribute-groups): Attribute groups for personal product matching
 
 allOf:
-  - $ref: "../api.yml#/components/schemas/Product-Base"
-  - $ref: "../api.yml#/components/schemas/Product-Misc"
-  - $ref: "../api.yml#/components/schemas/Product-Tags"
-  - $ref: "../api.yml#/components/schemas/Product-Images"
-  - $ref: "../api.yml#/components/schemas/Product-Eco-Score"
-  - $ref: "../api.yml#/components/schemas/Product-Ingredients"
-  - $ref: "../api.yml#/components/schemas/Product-Nutrition"
-  - $ref: "../api.yml#/components/schemas/Product-Nutriscore"
-  - $ref: "../api.yml#/components/schemas/Product-Quality"
-  - $ref: "../api.yml#/components/schemas/Product-Extended"
-  - $ref: "../api.yml#/components/schemas/Product-Metadata"
-  - $ref: "../api.yml#/components/schemas/Product-Knowledge-Panels"
-  - $ref: "../api.yml#/components/schemas/Product-Attribute-Groups"
+  - $ref: "../api.yaml#/components/schemas/Product-Base"
+  - $ref: "../api.yaml#/components/schemas/Product-Misc"
+  - $ref: "../api.yaml#/components/schemas/Product-Tags"
+  - $ref: "../api.yaml#/components/schemas/Product-Images"
+  - $ref: "../api.yaml#/components/schemas/Product-Eco-Score"
+  - $ref: "../api.yaml#/components/schemas/Product-Ingredients"
+  - $ref: "../api.yaml#/components/schemas/Product-Nutrition"
+  - $ref: "../api.yaml#/components/schemas/Product-Nutriscore"
+  - $ref: "../api.yaml#/components/schemas/Product-Quality"
+  - $ref: "../api.yaml#/components/schemas/Product-Extended"
+  - $ref: "../api.yaml#/components/schemas/Product-Metadata"
+  - $ref: "../api.yaml#/components/schemas/Product-Knowledge-Panels"
+  - $ref: "../api.yaml#/components/schemas/Product-Attribute-Groups"
diff --git a/docs/api/ref/schemas/product_meta.yaml b/docs/api/ref/schemas/product_meta.yaml
index f2fd93a44f336..1bb5bb1cc9aef 100644
--- a/docs/api/ref/schemas/product_meta.yaml
+++ b/docs/api/ref/schemas/product_meta.yaml
@@ -8,8 +8,7 @@ properties:
     description: |
       Date when the product was added (UNIX timestamp format).
       See also `entry_dates_tags`
-    example: |
-      1457680652
+    example: 1457680652
   creator:
     type: string
     description: |
diff --git a/docs/api/ref/schemas/product_v3.yaml b/docs/api/ref/schemas/product_v3.yaml
index a7bf6f35d5119..fd2aac4737035 100644
--- a/docs/api/ref/schemas/product_v3.yaml
+++ b/docs/api/ref/schemas/product_v3.yaml
@@ -20,16 +20,16 @@ description: |
   * [Product Attribute Groups](#cmp--schemas-product-attribute-groups): Attribute groups for personal product matching
 
 allOf:
-  - $ref: "../api.yml#/components/schemas/Product-Base"
-  - $ref: "../api.yml#/components/schemas/Product-Misc"
-  - $ref: "../api.yml#/components/schemas/Product-Tags"
+  - $ref: "../api.yaml#/components/schemas/Product-Base"
+  - $ref: "../api.yaml#/components/schemas/Product-Misc"
+  - $ref: "../api.yaml#/components/schemas/Product-Tags"
   - $ref: "./product_images_v3.yaml#/components/schemas/ProductImagesV3"
-  - $ref: "../api.yml#/components/schemas/Product-Eco-Score"
-  - $ref: "../api.yml#/components/schemas/Product-Ingredients"
-  - $ref: "../api.yml#/components/schemas/Product-Nutrition"
-  - $ref: "../api.yml#/components/schemas/Product-Nutriscore"
-  - $ref: "../api.yml#/components/schemas/Product-Quality"
-  - $ref: "../api.yml#/components/schemas/Product-Extended"
-  - $ref: "../api.yml#/components/schemas/Product-Metadata"
-  - $ref: "../api.yml#/components/schemas/Product-Knowledge-Panels"
-  - $ref: "../api.yml#/components/schemas/Product-Attribute-Groups"
+  - $ref: "../api.yaml#/components/schemas/Product-Eco-Score"
+  - $ref: "../api.yaml#/components/schemas/Product-Ingredients"
+  - $ref: "../api.yaml#/components/schemas/Product-Nutrition"
+  - $ref: "../api.yaml#/components/schemas/Product-Nutriscore"
+  - $ref: "../api.yaml#/components/schemas/Product-Quality"
+  - $ref: "../api.yaml#/components/schemas/Product-Extended"
+  - $ref: "../api.yaml#/components/schemas/Product-Metadata"
+  - $ref: "../api.yaml#/components/schemas/Product-Knowledge-Panels"
+  - $ref: "../api.yaml#/components/schemas/Product-Attribute-Groups"
diff --git a/docs/assets/api-rapidoc.html b/docs/assets/api-rapidoc.html
index 0d9eef5384350..8296ba258ce7f 100644
--- a/docs/assets/api-rapidoc.html
+++ b/docs/assets/api-rapidoc.html
@@ -31,7 +31,7 @@
 </head>
 <body>
   <rapi-doc
-    spec-url="https://raw.githubusercontent.com/openfoodfacts/openfoodfacts-server/main/docs/api/ref/api.yml"
+    spec-url="https://raw.githubusercontent.com/openfoodfacts/openfoodfacts-server/main/docs/api/ref/api.yaml"
     render-style="focus"
     schema-expand-level="5"
     default-schema-tab="schema"
diff --git a/html/images/lang/fr/.DS_Store b/html/images/lang/fr/.DS_Store
deleted file mode 100644
index d38179f13c091..0000000000000
Binary files a/html/images/lang/fr/.DS_Store and /dev/null differ
diff --git a/redocly.yaml b/redocly.yaml
index 706ebc85ccfc0..9bf69360c9135 100644
--- a/redocly.yaml
+++ b/redocly.yaml
@@ -3,9 +3,9 @@ theme:
     schemaDefinitionsTagName: Schemas
 apis:
   main:
-    root: docs/api/ref/api.yml
+    root: docs/api/ref/api.yaml
   test:
-    root: docs/api/ref/api-v3.yml
+    root: docs/api/ref/api-v3.yaml
 extends:
   - recommended
 rules:


Reviews:
user: login: stephanegigandet
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T07:55:56Z
user: login: stephanegigandet
body: Looks good to me!
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T07:57:37Z
user: login: JagjeevanAK
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T10:38:24Z
user: login: JagjeevanAK
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T13:07:36Z
user: login: alexgarel
body: @JagjeevanAK see my comments.

I did a commit to fix some errors. But I think the one with ingredients is a recursion problem. Maybe we are doing it wrong...
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-02T13:27:34Z
user: login: alexgarel
body: Thanks for working on that @JagjeevanAK.

I still have problems making lint OpenAPI change.

Also you should revert change to text files that are not related to OpenAPI linting.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-05T08:12:56Z
user: login: alexgarel
body: I have made some commits to fix small bugs. When I run spectral locally he was not able to use circular reference if I did not include the filename.

Also I put it in check_openapi, even if it means running it twice in CI (not a big deal).
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-26T13:26:29Z

Review comments:
diff_hunk: @@ -0,0 +1,33 @@
+name: OpenAPI Spec Linting
+
+on:
+  pull_request:
+    paths:
+      - 'docs/api/ref/api.yml'
path: .github/workflows/api-linting.yml
user: login: stephanegigandet
body: Shouldn't it be all of docs/api/ref/ ? e.g. if we change some schemas. There are a few .md files in there, but it's not a big deal I think. Or we could add all the subdirectories with OpenAPI components.
created_at: 2025-05-02T07:55:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,33 @@
+name: OpenAPI Spec Linting
+
+on:
+  pull_request:
+    paths:
+      - 'docs/api/ref/api.yml'
path: .github/workflows/api-linting.yml
user: login: JagjeevanAK
body: 100% you are correct, I just over looked it. I will commit changes soon.
created_at: 2025-05-02T10:38:24Z
author_association: CONTRIBUTOR
path: .github/workflows/api-linting.yml
user: login: alexgarel
body: We really need to have a way to run it locally. Ideally a docker command in the Makefile (maybe using a ready to use spectral image and bind mounting the right folder). 

And there I would use the same command "make lint_openapi"
created_at: 2025-05-02T11:36:33Z
author_association: MEMBER
diff_hunk: @@ -38,4 +38,4 @@ components:
         This structure gives the different ingredients and some information about them,
         like estimate on their quantity.
       items:
-        $ref: "#/components/schemas/Ingredient"
+        $ref: "./ingredient.yaml#/components/schemas/Ingredient"
path: docs/api/ref/schemas/ingredient.yaml
user: login: alexgarel
body: I find this a bit strange to put it like this while we already are in the right file. I hope documentation parser will all agree on this !
created_at: 2025-05-02T11:38:36Z
author_association: MEMBER
path: .github/workflows/api-linting.yml
user: login: alexgarel
body: ```bash
docker run --rm -v ./:/app  stoplight/spectral lint -r /app/.spectral.yaml  /app/docs/api/ref/api.yml /app/docs/api/ref/api-v3.yml
```
created_at: 2025-05-02T11:47:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,33 @@
+name: OpenAPI Spec Linting
+
+on:
+  pull_request:
+    paths:
+      - 'docs/api/ref/api.yml'
path: .github/workflows/api-linting.yml
user: login: JagjeevanAK
body: I tried that way but it is not working correctly, and according to my hit and trial method it tell's me that it spectral also look into the $ref linking as well.
created_at: 2025-05-02T13:07:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -424,6 +424,14 @@ check_openapi_v3:
 
 check_openapi: check_openapi_v2 check_openapi_v3
 
+lint_openapi:
+	@if [ "$(OS)" = "Darwin" ]; then \
+		echo "🥫 Linting OpenAPI is not supported on macOS"; \
path: Makefile
user: login: alexgarel
body: How is it, if it's using docker ?
created_at: 2025-05-05T08:04:42Z
author_association: MEMBER
path: COPYRIGHT
user: login: alexgarel
body: why do we have all those white space changes on .md, .svg, etc. ?

Can you revert them ?
created_at: 2025-05-05T08:08:10Z
author_association: MEMBER
diff_hunk: @@ -415,15 +415,23 @@ lint_taxonomies:
 check_openapi_v2:
 	docker run --rm \
 		-v ${PWD}:/local openapitools/openapi-generator-cli validate --recommend \
-		-i /local/docs/api/ref/api.yml
+		-i /local/docs/api/ref/api.yaml
 
 check_openapi_v3:
 	docker run --rm \
 		-v ${PWD}:/local openapitools/openapi-generator-cli validate --recommend \
-		-i /local/docs/api/ref/api-v3.yml
+		-i /local/docs/api/ref/api-v3.yaml
 
 check_openapi: check_openapi_v2 check_openapi_v3
 
+lint_openapi:
+	@if [ "$(OS)" = "Darwin" ]; then \
+		echo "🥫 Linting OpenAPI is not supported on macOS"; \
+	else \
+		echo "🥫 Linting taxonomies"; \
path: Makefile
user: login: alexgarel
body: ```suggestion
		echo "🥫 Linting OpenAPI files"; \
```
created_at: 2025-05-05T08:09:53Z
author_association: MEMBER
diff_hunk: @@ -415,15 +415,23 @@ lint_taxonomies:
 check_openapi_v2:
 	docker run --rm \
 		-v ${PWD}:/local openapitools/openapi-generator-cli validate --recommend \
-		-i /local/docs/api/ref/api.yml
+		-i /local/docs/api/ref/api.yaml
 
 check_openapi_v3:
 	docker run --rm \
 		-v ${PWD}:/local openapitools/openapi-generator-cli validate --recommend \
-		-i /local/docs/api/ref/api-v3.yml
+		-i /local/docs/api/ref/api-v3.yaml
 
 check_openapi: check_openapi_v2 check_openapi_v3
 
+lint_openapi:
+	@if [ "$(OS)" = "Darwin" ]; then \
+		echo "🥫 Linting OpenAPI is not supported on macOS"; \
+	else \
+		echo "🥫 Linting taxonomies"; \
+		docker run --rm -v $$(pwd):/app stoplight/spectral lint -r /app/.spectral.yaml /app/docs/api/ref/api.yml /app/docs/api/ref/api-v3.yml; \
path: Makefile
user: login: alexgarel
body: you forgot to rename extension:

```suggestion
		docker run --rm -v $$(pwd):/app stoplight/spectral lint -r /app/.spectral.yaml /app/docs/api/ref/api.yaml /app/docs/api/ref/api-v3.yaml; \
```

With this I don't have the make lint_openapi passing:
```
$ docker run --rm -v $(pwd):/app stoplight/spectral lint -r /app/.spectral.yaml /app/docs/api/ref/api.yaml /app/docs/api/ref/api-v3.yaml

/app/docs/api/ref/api.yaml
   1:1     error  invalid-ref            '#/components/schemas/Ingredients' does not exist  properties.ingredients.items.properties.ingredients.$ref
 531:13  warning  oas3-unused-component  Potentially unused component has been detected.    components.schemas.Product
 533:16  warning  oas3-unused-component  Potentially unused component has been detected.    components.schemas.Ingredient
 536:14  warning  oas3-unused-component  Potentially unused component has been detected.    components.schemas.Nutrient

✖ 4 problems (1 error, 3 warnings, 0 infos, 0 hints)
```
created_at: 2025-05-05T08:12:09Z
author_association: MEMBER

PR comments:
user: login: JagjeevanAK
created_at: 2025-05-01T14:08:57Z
body: This PR is open for review and discussion @alexgarel and @stephanegigandet.
author_association: CONTRIBUTOR
user: login: codecov-commenter
created_at: 2025-05-01T14:40:36Z
body: ## [Codecov](https://app.codecov.io/gh/openfoodfacts/openfoodfacts-server/pull/11847?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openfoodfacts) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 50.38%. Comparing base [(`e8deee5`)](https://app.codecov.io/gh/openfoodfacts/openfoodfacts-server/commit/e8deee55d3dbcca89b8edfbeb8745838b3b8fafb?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openfoodfacts) to head [(`0bb04d0`)](https://app.codecov.io/gh/openfoodfacts/openfoodfacts-server/commit/0bb04d063c9987a68d4887d3b3900a043959a742?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openfoodfacts).
> Report is 232 commits behind head on main.

:white_check_mark: All tests successful. No failed tests found.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main   #11847      +/-   ##
==========================================
+ Coverage   49.88%   50.38%   +0.49%     
==========================================
  Files          83       84       +1     
  Lines       22984    23011      +27     
  Branches     5508     5522      +14     
==========================================
+ Hits        11466    11593     +127     
+ Misses      10121     9996     -125     
- Partials     1397     1422      +25     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/openfoodfacts/openfoodfacts-server/pull/11847?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openfoodfacts).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openfoodfacts).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: JagjeevanAK
created_at: 2025-05-02T02:02:20Z
body: If this initial idea is accepted then, I am thinking of creating documentation to help other dev's to create the rules.
author_association: CONTRIBUTOR
user: login: JagjeevanAK
created_at: 2025-05-04T13:27:07Z
body: Keep in mind that, I have removed some `.DS_Store` files form codebase you will see some unexpected changes in some of the file which I have no intend to change.
author_association: CONTRIBUTOR
user: login: JagjeevanAK
created_at: 2025-05-05T14:25:19Z
body: Hey @stephanegigandet,
can you help to understand why unit test's are failing here ? as I have not touched the perl code. 
I think using `/update_test` will not be good in this senario.
author_association: CONTRIBUTOR
user: login: JagjeevanAK
created_at: 2025-05-08T14:09:50Z
body: Hey @alexgarel,

 I have cleaned the diff and solved the problem of file changes that  are unrelated to this PR.
author_association: CONTRIBUTOR
user: login: JagjeevanAK
created_at: 2025-05-12T14:16:38Z
body: @alexgarel As this PR is ready to merge and you can take look at,  I am waiting for suggestions.
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-05-26T13:28:09Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=openfoodfacts_openfoodfacts-server&pullRequest=11847) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=openfoodfacts_openfoodfacts-server&pullRequest=11847&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=openfoodfacts_openfoodfacts-server&pullRequest=11847&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=openfoodfacts_openfoodfacts-server&pullRequest=11847&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=openfoodfacts_openfoodfacts-server&pullRequest=11847&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=openfoodfacts_openfoodfacts-server&pullRequest=11847&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=openfoodfacts_openfoodfacts-server&pullRequest=11847)


author_association: NONE