Repository: fractal-analytics-platform/fractal-tasks-core (language: python)

Issue:
Take decision on Pydantic version for core library and dev tools.
(cc @lorenzocerrone  @jluethi)

We still have something to clarify concerning the Pydantic dependency version.

* After we fix #790 (e.g. as of #793), `fractal-tasks-core` can only coexist with Pydantic v2. The most natural approach here would be to change the dependency constraint to `pydantic>=2`.
* As part of #791, we were planning to let the JSON-schema generation tools still work for Pydantic V1 as well. And in fact in #801 I've been building towards a set of flexible schema-generation tools, which would generate either `pydantic_v1` or `pydantic_v2` manifests. Note that in this scenario we would need fractal-tasks-core to coexist with Pydantic v1 package, which is in contrast with the previous point.
* A provisional workaround could go through import-time checks. However, since `dev` is nested within the core library main package (`fractal_tasks_core`) we don't have the option to be more flexible for the `dev` subpackage while being less flexible for its parent package. This is based on my understanding of import checks (see details below), but I'm curious to know about other options.

At the moments I don't see a way out. We would have to make the next `fractal-tasks-core` version depend on Pydantic V2. This means that v1.1.0 could be used as a transitional version, but package developers who import from `fractal_tasks_core` would not be able to use Pydantic V1 (this is probably a minor issue).
Two options would remain in terms of schema-generation tools:
1. We keep them in sync with `fractal_tasks_core`, which means that they will only generate pydantic v2 schemas from now on.
2. We move `fractal_tasks_core.dev` into a separate package, where we can keep its dependencies more flexible than the `fractal_tasks_core` ones.

I see the benefit of option 2, overall, but it's something that would clearly take more work. Option 1 is actually a no-effort change for any task developer who was not pinning their pydantic dependency, and probably would only affect very few users (who, for the moment, would need to pin `fractal-tasks-core` to be able to stick with Pydantic V1).


---

My understanding is based on the following example
```console
$ tree .
.
└── fractal_tasks_core
    ├── core_library_module.py
    ├── dev
    │   ├── dev_module.py
    │   └── __init__.py
    └── __init__.py

2 directories, 4 files

$ cat fractal_tasks_core/__init__.py 
print("Printing from fractal_tasks_core/__init__.py")

$ cat fractal_tasks_core/dev/__init__.py 
print("Printing from fractal_tasks_core/dev/__init__.py")

$ python -c 'import fractal_tasks_core.dev.dev_module'
Printing from fractal_tasks_core/__init__.py
Printing from fractal_tasks_core/dev/__init__.py
```
where I am not able to bypass the import of the parent package when importing from `dev`.

I am happy to hear about something I missed.
Take decision on Pydantic version for core library and dev tools.
(cc @lorenzocerrone  @jluethi)

We still have something to clarify concerning the Pydantic dependency version.

* After we fix #790 (e.g. as of #793), `fractal-tasks-core` can only coexist with Pydantic v2. The most natural approach here would be to change the dependency constraint to `pydantic>=2`.
* As part of #791, we were planning to let the JSON-schema generation tools still work for Pydantic V1 as well. And in fact in #801 I've been building towards a set of flexible schema-generation tools, which would generate either `pydantic_v1` or `pydantic_v2` manifests. Note that in this scenario we would need fractal-tasks-core to coexist with Pydantic v1 package, which is in contrast with the previous point.
* A provisional workaround could go through import-time checks. However, since `dev` is nested within the core library main package (`fractal_tasks_core`) we don't have the option to be more flexible for the `dev` subpackage while being less flexible for its parent package. This is based on my understanding of import checks (see details below), but I'm curious to know about other options.

At the moments I don't see a way out. We would have to make the next `fractal-tasks-core` version depend on Pydantic V2. This means that v1.1.0 could be used as a transitional version, but package developers who import from `fractal_tasks_core` would not be able to use Pydantic V1 (this is probably a minor issue).
Two options would remain in terms of schema-generation tools:
1. We keep them in sync with `fractal_tasks_core`, which means that they will only generate pydantic v2 schemas from now on.
2. We move `fractal_tasks_core.dev` into a separate package, where we can keep its dependencies more flexible than the `fractal_tasks_core` ones.

I see the benefit of option 2, overall, but it's something that would clearly take more work. Option 1 is actually a no-effort change for any task developer who was not pinning their pydantic dependency, and probably would only affect very few users (who, for the moment, would need to pin `fractal-tasks-core` to be able to stick with Pydantic V1).


---

My understanding is based on the following example
```console
$ tree .
.
└── fractal_tasks_core
    ├── core_library_module.py
    ├── dev
    │   ├── dev_module.py
    │   └── __init__.py
    └── __init__.py

2 directories, 4 files

$ cat fractal_tasks_core/__init__.py 
print("Printing from fractal_tasks_core/__init__.py")

$ cat fractal_tasks_core/dev/__init__.py 
print("Printing from fractal_tasks_core/dev/__init__.py")

$ python -c 'import fractal_tasks_core.dev.dev_module'
Printing from fractal_tasks_core/__init__.py
Printing from fractal_tasks_core/dev/__init__.py
```
where I am not able to bypass the import of the parent package when importing from `dev`.

I am happy to hear about something I missed.
Pydantic v2 second attempt jsonschema
## Checklist before merging
- [x] I added an appropriate entry to `CHANGELOG.md`



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/manifest_external_packages.yml b/.github/workflows/manifest_external_packages.yml
index e4a471d72..d3b26c560 100644
--- a/.github/workflows/manifest_external_packages.yml
+++ b/.github/workflows/manifest_external_packages.yml
@@ -23,36 +23,46 @@ jobs:
 
         package: [skip]
         github_repo: [skip]
+        github_branch: [skip]
         manifest_path: [skip]
         cmd_install: [skip]
         cmd_create_manifest: [skip]
+        custom_dependencies: [skip]
 
         include:
 
           - package: scMultipleX
             github_repo: fmi-basel/gliberal-scMultipleX
+            github_branch: main
             manifest_path: src/scmultiplex/__FRACTAL_MANIFEST__.json
             cmd_install: 'python -m pip install -e .[fractal-tasks]'
             cmd_create_manifest: 'python src/scmultiplex/dev/create_manifest.py'
+            custom_dependencies: 'image_registration'
 
           - package: fractal-helper-tasks
             github_repo: jluethi/fractal-helper-tasks
+            github_branch: main
             manifest_path: src/fractal_helper_tasks/__FRACTAL_MANIFEST__.json
             cmd_install: 'python -m pip install -e .'
             cmd_create_manifest: 'python src/fractal_helper_tasks/dev/create_manifest.py'
+            custom_dependencies: ''
 
           - package: APx_fractal_task_collection
             github_repo: Apricot-Therapeutics/APx_fractal_task_collection
+            github_branch: pydantic_v2
             manifest_path: src/apx_fractal_task_collection/__FRACTAL_MANIFEST__.json
             cmd_install: 'python -m pip install -e .'
             cmd_create_manifest: 'python src/apx_fractal_task_collection/dev/update_manifest.py'
+            custom_dependencies: ''
 
         exclude:
           - package: skip
             github_repo: skip
+            github_branch: skip
             manifest_path: skip
             cmd_install: skip
             cmd_create_manifest: skip
+            custom_dependencies: skip
 
     steps:
 
@@ -63,6 +73,7 @@ jobs:
         uses: actions/checkout@v4
         with:
           repository: ${{ matrix.github_repo }}
+          ref: ${{ matrix.github_branch }}
 
       - uses: actions/setup-python@v5
         with:
@@ -75,11 +86,29 @@ jobs:
       - name: Install package
         run:  ${{ matrix.cmd_install }}
 
+      - name: Get current branch of `fractal-tasks-core`
+        uses: actions/checkout@v4
+        with:
+          path: fractal-tasks-core
+
+      - name: Install current fractal-tasks-core (this may fail)
+        run: python -m pip install -e ./fractal-tasks-core
+
+      - name: Install custom additional dependencies (see issue 803)
+        if: ${{ matrix.custom_dependencies != '' }}
+        run: python -m pip install ${{ matrix.custom_dependencies }}
+
       - name: Create manifest
         run: ${{ matrix.cmd_create_manifest }}
 
+      - name: Setup friendly diff style
+        run: echo "*.json diff=json" >> .gitattributes && git config diff.json.textconv "jq --sort-keys '.' \$1"
+
       - name: Run git diff for manifest
         run: git diff ${{ matrix.manifest_path }}
 
+      - name: Clean up before checking repo status
+        run: rm -rf fractal-tasks-core .gitattributes
+
       - name: Check repo status
         run: if [[ -z $(git status -s) ]]; then echo "Clean status"; else echo "Dirty status"; git status; exit 1; fi
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4a3913c2f..b061c92a3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,20 @@
 **Note**: Numbers like (\#123) point to closed Pull Requests on the fractal-tasks-core repository.
 
+# 1.2.0 (unreleased)
+
+* Core-library and tasks:
+    * Switch all core models to Pydantic V2 (\#793).
+* JSON Schema generation tools:
+    * Move JSON-Schema tools to Pydantic V2 (\#793).
+* Testing:
+    * Remove dependency on `pytest-pretty` (\#793).
+    * Update `manifest_external_packages.yml` GitHub Action so that it installs the current `fractal-tasks-core` (\#793).
+
 # 1.1.1
 
 * Tasks:
-    * Fix issue with masked ROI & relabeling in Cellpose task (\#785).
-    * Fix issue with masking ROI label types in `masked_loading_wrapper` for Cellpose task (\#785).
+    * Fix issue with masked ROI & relabeling in Cellpose task (\#786).
+    * Fix issue with masking ROI label types in `masked_loading_wrapper` for Cellpose task (\#786).
     * Enable workaround to support yx images in Cellpose task (\#789).
     * Fix error handling in `calculate_registration_image_based` (\#799).
     * Fix minor issues with call-signature and type hints in `calculate_registration_image_based` (\#799).
@@ -22,8 +32,8 @@
     * Refactor Cellpose Task inputs: Support independent normalization of 2 input channels in the Cellpose task (\#738).
     * Rename `task.cellpose_transforms` into `tasks.cellpose_utils` (\#738).
     * Fix wrong repeated overlap checks for bounding-boxes in Cellpose task (\#778).
-    * Fix minor MIP issues related to plate metadata and expecting acquisition metadata in all NGFF plates(\#781).
-    * Add `chi2_shift` option to Calculate Registration (image-based) task(\#741).
+    * Fix minor MIP issues related to plate metadata and expecting acquisition metadata in all NGFF plates (\#781).
+    * Add `chi2_shift` option to Calculate Registration (image-based) task (\#741).
 * Development:
     * Switch to transitional pydantic.v1 imports, changes pydantic requirement to `==1.10.16` or `>=2.6.3` (\#760).
     * Support JSON-Schema generation for `Enum` task arguments (\#749).
@@ -35,6 +45,8 @@
     * Test manifest creation for three other tasks packages (\#763).
 * NGFF subpackage
     * Fix Plate model to correspond better to 0.4.0 NGFF spec: Now makes acquisition metadata optional (\#781).
+* Dependencies:
+    * Add `image_registration` within `fractal-tasks` extra (\#741).
 
 # 1.0.2
 
diff --git a/fractal_tasks_core/__FRACTAL_MANIFEST__.json b/fractal_tasks_core/__FRACTAL_MANIFEST__.json
index 824f18e3a..e8a1788c3 100644
--- a/fractal_tasks_core/__FRACTAL_MANIFEST__.json
+++ b/fractal_tasks_core/__FRACTAL_MANIFEST__.json
@@ -14,15 +14,104 @@
         "mem": 4000
       },
       "args_schema_non_parallel": {
-        "title": "CellvoyagerToOmeZarrInit",
-        "type": "object",
+        "$defs": {
+          "OmeroChannel": {
+            "description": "Custom class for Omero channels, based on OME-NGFF v0.4.",
+            "properties": {
+              "wavelength_id": {
+                "title": "Wavelength Id",
+                "type": "string",
+                "description": "Unique ID for the channel wavelength, e.g. `A01_C01`."
+              },
+              "index": {
+                "title": "Index",
+                "type": "integer",
+                "description": "Do not change. For internal use only."
+              },
+              "label": {
+                "title": "Label",
+                "type": "string",
+                "description": "Name of the channel."
+              },
+              "window": {
+                "allOf": [
+                  {
+                    "$ref": "#/$defs/Window"
+                  }
+                ],
+                "title": "Window",
+                "description": "Optional `Window` object to set default display settings for napari."
+              },
+              "color": {
+                "title": "Color",
+                "type": "string",
+                "description": "Optional hex colormap to display the channel in napari (it must be of length 6, e.g. `00FFFF`)."
+              },
+              "active": {
+                "default": true,
+                "title": "Active",
+                "type": "boolean",
+                "description": "Should this channel be shown in the viewer?"
+              },
+              "coefficient": {
+                "default": 1,
+                "title": "Coefficient",
+                "type": "integer",
+                "description": "Do not change. Omero-channel attribute."
+              },
+              "inverted": {
+                "default": false,
+                "title": "Inverted",
+                "type": "boolean",
+                "description": "Do not change. Omero-channel attribute."
+              }
+            },
+            "required": [
+              "wavelength_id"
+            ],
+            "title": "OmeroChannel",
+            "type": "object"
+          },
+          "Window": {
+            "description": "Custom class for Omero-channel window, based on OME-NGFF v0.4.",
+            "properties": {
+              "min": {
+                "title": "Min",
+                "type": "integer",
+                "description": "Do not change. It will be set to `0` by default."
+              },
+              "max": {
+                "title": "Max",
+                "type": "integer",
+                "description": "Do not change. It will be set according to bit-depth of the images by default (e.g. 65535 for 16 bit images)."
+              },
+              "start": {
+                "title": "Start",
+                "type": "integer",
+                "description": "Lower-bound rescaling value for visualization."
+              },
+              "end": {
+                "title": "End",
+                "type": "integer",
+                "description": "Upper-bound rescaling value for visualization."
+              }
+            },
+            "required": [
+              "start",
+              "end"
+            ],
+            "title": "Window",
+            "type": "object"
+          }
+        },
+        "additionalProperties": false,
         "properties": {
           "zarr_urls": {
-            "title": "Zarr Urls",
-            "type": "array",
             "items": {
               "type": "string"
             },
+            "title": "Zarr Urls",
+            "type": "array",
             "description": "List of paths or urls to the individual OME-Zarr image to be processed. Not used by the converter task. (standard argument for Fractal tasks, managed by Fractal server)."
           },
           "zarr_dir": {
@@ -31,44 +120,44 @@
             "description": "path of the directory where the new OME-Zarrs will be created. (standard argument for Fractal tasks, managed by Fractal server)."
           },
           "image_dirs": {
-            "title": "Image Dirs",
-            "type": "array",
             "items": {
               "type": "string"
             },
+            "title": "Image Dirs",
+            "type": "array",
             "description": "list of paths to the folders that contains the Cellvoyager image files. Each entry is a path to a folder that contains the image files themselves for a multiwell plate and the MeasurementData & MeasurementDetail metadata files."
           },
           "allowed_channels": {
-            "title": "Allowed Channels",
-            "type": "array",
             "items": {
-              "$ref": "#/definitions/OmeroChannel"
+              "$ref": "#/$defs/OmeroChannel"
             },
+            "title": "Allowed Channels",
+            "type": "array",
             "description": "A list of `OmeroChannel` s, where each channel must include the `wavelength_id` attribute and where the `wavelength_id` values must be unique across the list."
           },
           "image_glob_patterns": {
-            "title": "Image Glob Patterns",
-            "type": "array",
             "items": {
               "type": "string"
             },
+            "title": "Image Glob Patterns",
+            "type": "array",
             "description": "If specified, only parse images with filenames that match with all these patterns. Patterns must be defined as in https://docs.python.org/3/library/fnmatch.html, Example: `image_glob_pattern=[\"*_B03_*\"]` => only process well B03 `image_glob_pattern=[\"*_C09_*\", \"*F016*\", \"*Z[0-5][0-9]C*\"]` => only process well C09, field of view 16 and Z planes 0-59."
           },
           "num_levels": {
-            "title": "Num Levels",
             "default": 5,
+            "title": "Num Levels",
             "type": "integer",
             "description": "Number of resolution-pyramid levels. If set to `5`, there will be the full-resolution level and 4 levels of downsampled images."
           },
           "coarsening_xy": {
-            "title": "Coarsening Xy",
             "default": 2,
+            "title": "Coarsening Xy",
             "type": "integer",
             "description": "Linear coarsening factor between subsequent levels. If set to `2`, level 1 is 2x downsampled, level 2 is 4x downsampled etc."
           },
           "image_extension": {
-            "title": "Image Extension",
             "default": "tif",
+            "title": "Image Extension",
             "type": "string",
             "description": "Filename extension of images (e.g. `\"tif\"` or `\"png\"`)"
           },
@@ -78,8 +167,8 @@
             "description": "If `None`, parse Yokogawa metadata from mrf/mlf files in the input_path folder; else, the full path to a csv file containing the parsed metadata table."
           },
           "overwrite": {
-            "title": "Overwrite",
             "default": false,
+            "title": "Overwrite",
             "type": "boolean",
             "description": "If `True`, overwrite the task output."
           }
@@ -90,119 +179,13 @@
           "image_dirs",
           "allowed_channels"
         ],
-        "additionalProperties": false,
-        "definitions": {
-          "Window": {
-            "title": "Window",
-            "description": "Custom class for Omero-channel window, based on OME-NGFF v0.4.",
-            "type": "object",
-            "properties": {
-              "min": {
-                "title": "Min",
-                "type": "integer",
-                "description": "Do not change. It will be set to `0` by default."
-              },
-              "max": {
-                "title": "Max",
-                "type": "integer",
-                "description": "Do not change. It will be set according to bit-depth of the images by default (e.g. 65535 for 16 bit images)."
-              },
-              "start": {
-                "title": "Start",
-                "type": "integer",
-                "description": "Lower-bound rescaling value for visualization."
-              },
-              "end": {
-                "title": "End",
-                "type": "integer",
-                "description": "Upper-bound rescaling value for visualization."
-              }
-            },
-            "required": [
-              "start",
-              "end"
-            ]
-          },
-          "OmeroChannel": {
-            "title": "OmeroChannel",
-            "description": "Custom class for Omero channels, based on OME-NGFF v0.4.",
-            "type": "object",
-            "properties": {
-              "wavelength_id": {
-                "title": "Wavelength Id",
-                "type": "string",
-                "description": "Unique ID for the channel wavelength, e.g. `A01_C01`."
-              },
-              "index": {
-                "title": "Index",
-                "type": "integer",
-                "description": "Do not change. For internal use only."
-              },
-              "label": {
-                "title": "Label",
-                "type": "string",
-                "description": "Name of the channel."
-              },
-              "window": {
-                "$ref": "#/definitions/Window",
-                "title": "Window",
-                "description": "Optional `Window` object to set default display settings for napari."
-              },
-              "color": {
-                "title": "Color",
-                "type": "string",
-                "description": "Optional hex colormap to display the channel in napari (it must be of length 6, e.g. `00FFFF`)."
-              },
-              "active": {
-                "title": "Active",
-                "default": true,
-                "type": "boolean",
-                "description": "Should this channel be shown in the viewer?"
-              },
-              "coefficient": {
-                "title": "Coefficient",
-                "default": 1,
-                "type": "integer",
-                "description": "Do not change. Omero-channel attribute."
-              },
-              "inverted": {
-                "title": "Inverted",
-                "default": false,
-                "type": "boolean",
-                "description": "Do not change. Omero-channel attribute."
-              }
-            },
-            "required": [
-              "wavelength_id"
-            ]
-          }
-        }
+        "type": "object",
+        "title": "CellvoyagerToOmeZarrInit"
       },
       "args_schema_parallel": {
-        "title": "CellvoyagerToOmeZarrCompute",
-        "type": "object",
-        "properties": {
-          "zarr_url": {
-            "title": "Zarr Url",
-            "type": "string",
-            "description": "Path or url to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "init_args": {
-            "$ref": "#/definitions/InitArgsCellVoyager",
-            "title": "Init_Args",
-            "description": "Intialization arguments provided by `create_cellvoyager_ome_zarr_init`."
-          }
-        },
-        "required": [
-          "zarr_url",
-          "init_args"
-        ],
-        "additionalProperties": false,
-        "definitions": {
+        "$defs": {
           "InitArgsCellVoyager": {
-            "title": "InitArgsCellVoyager",
             "description": "Arguments to be passed from cellvoyager converter init to compute",
-            "type": "object",
             "properties": {
               "image_dir": {
                 "title": "Image Dir",
@@ -221,11 +204,11 @@
                 "type": "string"
               },
               "image_glob_patterns": {
-                "title": "Image Glob Patterns",
-                "type": "array",
                 "items": {
                   "type": "string"
-                }
+                },
+                "title": "Image Glob Patterns",
+                "type": "array"
               },
               "acquisition": {
                 "title": "Acquisition",
@@ -237,9 +220,30 @@
               "plate_prefix",
               "well_ID",
               "image_extension"
-            ]
+            ],
+            "title": "InitArgsCellVoyager",
+            "type": "object"
           }
-        }
+        },
+        "additionalProperties": false,
+        "properties": {
+          "zarr_url": {
+            "title": "Zarr Url",
+            "type": "string",
+            "description": "Path or url to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
+          },
+          "init_args": {
+            "$ref": "#/$defs/InitArgsCellVoyager",
+            "title": "Init Args",
+            "description": "Intialization arguments provided by `create_cellvoyager_ome_zarr_init`."
+          }
+        },
+        "required": [
+          "zarr_url",
+          "init_args"
+        ],
+        "type": "object",
+        "title": "CellvoyagerToOmeZarrCompute"
       },
       "docs_info": "## cellvoyager_to_ome_zarr_init\nCreate a OME-NGFF zarr folder, without reading/writing image data.\n\nFind plates (for each folder in input_paths):\n\n- glob image files,\n- parse metadata from image filename to identify plates,\n- identify populated channels.\n\nCreate a zarr folder (for each plate):\n\n- parse mlf metadata,\n- identify wells and field of view (FOV),\n- create FOV ZARR,\n- verify that channels are uniform (i.e., same channels).\n## cellvoyager_to_ome_zarr_compute\nConvert Yokogawa output (png, tif) to zarr file.\n\nThis task is run after an init task (typically\n`cellvoyager_to_ome_zarr_init` or\n`cellvoyager_to_ome_zarr_init_multiplex`), and it populates the empty\nOME-Zarr files that were prepared.\n\nNote that the current task always overwrites existing data. To avoid this\nbehavior, set the `overwrite` argument of the init task to `False`.\n",
       "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core"
@@ -257,113 +261,33 @@
         "mem": 4000
       },
       "args_schema_non_parallel": {
-        "title": "CellvoyagerToOmeZarrInitMultiplex",
-        "type": "object",
-        "properties": {
-          "zarr_urls": {
-            "title": "Zarr Urls",
-            "type": "array",
-            "items": {
-              "type": "string"
-            },
-            "description": "List of paths or urls to the individual OME-Zarr image to be processed. Not used by the converter task. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "zarr_dir": {
-            "title": "Zarr Dir",
-            "type": "string",
-            "description": "path of the directory where the new OME-Zarrs will be created. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "acquisitions": {
-            "title": "Acquisitions",
-            "type": "object",
-            "additionalProperties": {
-              "$ref": "#/definitions/MultiplexingAcquisition"
-            },
-            "description": "dictionary of acquisitions. Each key is the acquisition identifier (normally 0, 1, 2, 3 etc.). Each item defines the acquisition by providing the image_dir and the allowed_channels."
-          },
-          "image_glob_patterns": {
-            "title": "Image Glob Patterns",
-            "type": "array",
-            "items": {
-              "type": "string"
-            },
-            "description": "If specified, only parse images with filenames that match with all these patterns. Patterns must be defined as in https://docs.python.org/3/library/fnmatch.html, Example: `image_glob_pattern=[\"*_B03_*\"]` => only process well B03 `image_glob_pattern=[\"*_C09_*\", \"*F016*\", \"*Z[0-5][0-9]C*\"]` => only process well C09, field of view 16 and Z planes 0-59."
-          },
-          "num_levels": {
-            "title": "Num Levels",
-            "default": 5,
-            "type": "integer",
-            "description": "Number of resolution-pyramid levels. If set to `5`, there will be the full-resolution level and 4 levels of downsampled images."
-          },
-          "coarsening_xy": {
-            "title": "Coarsening Xy",
-            "default": 2,
-            "type": "integer",
-            "description": "Linear coarsening factor between subsequent levels. If set to `2`, level 1 is 2x downsampled, level 2 is 4x downsampled etc."
-          },
-          "image_extension": {
-            "title": "Image Extension",
-            "default": "tif",
-            "type": "string",
-            "description": "Filename extension of images (e.g. `\"tif\"` or `\"png\"`)."
-          },
-          "metadata_table_files": {
-            "title": "Metadata Table Files",
-            "type": "object",
-            "additionalProperties": {
-              "type": "string"
-            },
-            "description": "If `None`, parse Yokogawa metadata from mrf/mlf files in the input_path folder; else, a dictionary of key-value pairs like `(acquisition, path)` with `acquisition` a string like the key of the `acquisitions` dict and `path` pointing to a csv file containing the parsed metadata table."
-          },
-          "overwrite": {
-            "title": "Overwrite",
-            "default": false,
-            "type": "boolean",
-            "description": "If `True`, overwrite the task output."
-          }
-        },
-        "required": [
-          "zarr_urls",
-          "zarr_dir",
-          "acquisitions"
-        ],
-        "additionalProperties": false,
-        "definitions": {
-          "Window": {
-            "title": "Window",
-            "description": "Custom class for Omero-channel window, based on OME-NGFF v0.4.",
-            "type": "object",
+        "$defs": {
+          "MultiplexingAcquisition": {
+            "description": "Input class for Multiplexing Cellvoyager converter",
             "properties": {
-              "min": {
-                "title": "Min",
-                "type": "integer",
-                "description": "Do not change. It will be set to `0` by default."
-              },
-              "max": {
-                "title": "Max",
-                "type": "integer",
-                "description": "Do not change. It will be set according to bit-depth of the images by default (e.g. 65535 for 16 bit images)."
-              },
-              "start": {
-                "title": "Start",
-                "type": "integer",
-                "description": "Lower-bound rescaling value for visualization."
+              "image_dir": {
+                "title": "Image Dir",
+                "type": "string",
+                "description": "Path to the folder that contains the Cellvoyager image files for that acquisition and the MeasurementData & MeasurementDetail metadata files."
               },
-              "end": {
-                "title": "End",
-                "type": "integer",
-                "description": "Upper-bound rescaling value for visualization."
+              "allowed_channels": {
+                "items": {
+                  "$ref": "#/$defs/OmeroChannel"
+                },
+                "title": "Allowed Channels",
+                "type": "array",
+                "description": "A list of `OmeroChannel` objects, where each channel must include the `wavelength_id` attribute and where the `wavelength_id` values must be unique across the list."
               }
             },
             "required": [
-              "start",
-              "end"
-            ]
+              "image_dir",
+              "allowed_channels"
+            ],
+            "title": "MultiplexingAcquisition",
+            "type": "object"
           },
           "OmeroChannel": {
-            "title": "OmeroChannel",
             "description": "Custom class for Omero channels, based on OME-NGFF v0.4.",
-            "type": "object",
             "properties": {
               "wavelength_id": {
                 "title": "Wavelength Id",
@@ -381,7 +305,11 @@
                 "description": "Name of the channel."
               },
               "window": {
-                "$ref": "#/definitions/Window",
+                "allOf": [
+                  {
+                    "$ref": "#/$defs/Window"
+                  }
+                ],
                 "title": "Window",
                 "description": "Optional `Window` object to set default display settings for napari."
               },
@@ -391,79 +319,138 @@
                 "description": "Optional hex colormap to display the channel in napari (it must be of length 6, e.g. `00FFFF`)."
               },
               "active": {
-                "title": "Active",
                 "default": true,
+                "title": "Active",
                 "type": "boolean",
                 "description": "Should this channel be shown in the viewer?"
               },
               "coefficient": {
-                "title": "Coefficient",
                 "default": 1,
+                "title": "Coefficient",
                 "type": "integer",
                 "description": "Do not change. Omero-channel attribute."
               },
               "inverted": {
-                "title": "Inverted",
                 "default": false,
+                "title": "Inverted",
                 "type": "boolean",
                 "description": "Do not change. Omero-channel attribute."
               }
             },
             "required": [
               "wavelength_id"
-            ]
+            ],
+            "title": "OmeroChannel",
+            "type": "object"
           },
-          "MultiplexingAcquisition": {
-            "title": "MultiplexingAcquisition",
-            "description": "Input class for Multiplexing Cellvoyager converter",
-            "type": "object",
+          "Window": {
+            "description": "Custom class for Omero-channel window, based on OME-NGFF v0.4.",
             "properties": {
-              "image_dir": {
-                "title": "Image Dir",
-                "type": "string",
-                "description": "Path to the folder that contains the Cellvoyager image files for that acquisition and the MeasurementData & MeasurementDetail metadata files."
+              "min": {
+                "title": "Min",
+                "type": "integer",
+                "description": "Do not change. It will be set to `0` by default."
               },
-              "allowed_channels": {
-                "title": "Allowed Channels",
-                "type": "array",
-                "items": {
-                  "$ref": "#/definitions/OmeroChannel"
-                },
-                "description": "A list of `OmeroChannel` objects, where each channel must include the `wavelength_id` attribute and where the `wavelength_id` values must be unique across the list."
+              "max": {
+                "title": "Max",
+                "type": "integer",
+                "description": "Do not change. It will be set according to bit-depth of the images by default (e.g. 65535 for 16 bit images)."
+              },
+              "start": {
+                "title": "Start",
+                "type": "integer",
+                "description": "Lower-bound rescaling value for visualization."
+              },
+              "end": {
+                "title": "End",
+                "type": "integer",
+                "description": "Upper-bound rescaling value for visualization."
               }
             },
             "required": [
-              "image_dir",
-              "allowed_channels"
-            ]
+              "start",
+              "end"
+            ],
+            "title": "Window",
+            "type": "object"
           }
-        }
-      },
-      "args_schema_parallel": {
-        "title": "CellvoyagerToOmeZarrCompute",
-        "type": "object",
+        },
+        "additionalProperties": false,
         "properties": {
-          "zarr_url": {
-            "title": "Zarr Url",
+          "zarr_urls": {
+            "items": {
+              "type": "string"
+            },
+            "title": "Zarr Urls",
+            "type": "array",
+            "description": "List of paths or urls to the individual OME-Zarr image to be processed. Not used by the converter task. (standard argument for Fractal tasks, managed by Fractal server)."
+          },
+          "zarr_dir": {
+            "title": "Zarr Dir",
             "type": "string",
-            "description": "Path or url to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
+            "description": "path of the directory where the new OME-Zarrs will be created. (standard argument for Fractal tasks, managed by Fractal server)."
           },
-          "init_args": {
-            "$ref": "#/definitions/InitArgsCellVoyager",
-            "title": "Init_Args",
-            "description": "Intialization arguments provided by `create_cellvoyager_ome_zarr_init`."
+          "acquisitions": {
+            "additionalProperties": {
+              "$ref": "#/$defs/MultiplexingAcquisition"
+            },
+            "title": "Acquisitions",
+            "type": "object",
+            "description": "dictionary of acquisitions. Each key is the acquisition identifier (normally 0, 1, 2, 3 etc.). Each item defines the acquisition by providing the image_dir and the allowed_channels."
+          },
+          "image_glob_patterns": {
+            "items": {
+              "type": "string"
+            },
+            "title": "Image Glob Patterns",
+            "type": "array",
+            "description": "If specified, only parse images with filenames that match with all these patterns. Patterns must be defined as in https://docs.python.org/3/library/fnmatch.html, Example: `image_glob_pattern=[\"*_B03_*\"]` => only process well B03 `image_glob_pattern=[\"*_C09_*\", \"*F016*\", \"*Z[0-5][0-9]C*\"]` => only process well C09, field of view 16 and Z planes 0-59."
+          },
+          "num_levels": {
+            "default": 5,
+            "title": "Num Levels",
+            "type": "integer",
+            "description": "Number of resolution-pyramid levels. If set to `5`, there will be the full-resolution level and 4 levels of downsampled images."
+          },
+          "coarsening_xy": {
+            "default": 2,
+            "title": "Coarsening Xy",
+            "type": "integer",
+            "description": "Linear coarsening factor between subsequent levels. If set to `2`, level 1 is 2x downsampled, level 2 is 4x downsampled etc."
+          },
+          "image_extension": {
+            "default": "tif",
+            "title": "Image Extension",
+            "type": "string",
+            "description": "Filename extension of images (e.g. `\"tif\"` or `\"png\"`)."
+          },
+          "metadata_table_files": {
+            "additionalProperties": {
+              "type": "string"
+            },
+            "title": "Metadata Table Files",
+            "type": "object",
+            "description": "If `None`, parse Yokogawa metadata from mrf/mlf files in the input_path folder; else, a dictionary of key-value pairs like `(acquisition, path)` with `acquisition` a string like the key of the `acquisitions` dict and `path` pointing to a csv file containing the parsed metadata table."
+          },
+          "overwrite": {
+            "default": false,
+            "title": "Overwrite",
+            "type": "boolean",
+            "description": "If `True`, overwrite the task output."
           }
         },
         "required": [
-          "zarr_url",
-          "init_args"
+          "zarr_urls",
+          "zarr_dir",
+          "acquisitions"
         ],
-        "additionalProperties": false,
-        "definitions": {
+        "type": "object",
+        "title": "CellvoyagerToOmeZarrInitMultiplex"
+      },
+      "args_schema_parallel": {
+        "$defs": {
           "InitArgsCellVoyager": {
-            "title": "InitArgsCellVoyager",
             "description": "Arguments to be passed from cellvoyager converter init to compute",
-            "type": "object",
             "properties": {
               "image_dir": {
                 "title": "Image Dir",
@@ -482,11 +469,11 @@
                 "type": "string"
               },
               "image_glob_patterns": {
-                "title": "Image Glob Patterns",
-                "type": "array",
                 "items": {
                   "type": "string"
-                }
+                },
+                "title": "Image Glob Patterns",
+                "type": "array"
               },
               "acquisition": {
                 "title": "Acquisition",
@@ -498,9 +485,30 @@
               "plate_prefix",
               "well_ID",
               "image_extension"
-            ]
+            ],
+            "title": "InitArgsCellVoyager",
+            "type": "object"
+          }
+        },
+        "additionalProperties": false,
+        "properties": {
+          "zarr_url": {
+            "title": "Zarr Url",
+            "type": "string",
+            "description": "Path or url to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
+          },
+          "init_args": {
+            "$ref": "#/$defs/InitArgsCellVoyager",
+            "title": "Init Args",
+            "description": "Intialization arguments provided by `create_cellvoyager_ome_zarr_init`."
           }
-        }
+        },
+        "required": [
+          "zarr_url",
+          "init_args"
+        ],
+        "type": "object",
+        "title": "CellvoyagerToOmeZarrCompute"
       },
       "docs_info": "## cellvoyager_to_ome_zarr_init_multiplex\nCreate OME-NGFF structure and metadata to host a multiplexing dataset.\n\nThis task takes a set of image folders (i.e. different multiplexing\nacquisitions) and build the internal structure and metadata of a OME-NGFF\nzarr group, without actually loading/writing the image data.\n\nEach element in input_paths should be treated as a different acquisition.\n## cellvoyager_to_ome_zarr_compute\nConvert Yokogawa output (png, tif) to zarr file.\n\nThis task is run after an init task (typically\n`cellvoyager_to_ome_zarr_init` or\n`cellvoyager_to_ome_zarr_init_multiplex`), and it populates the empty\nOME-Zarr files that were prepared.\n\nNote that the current task always overwrites existing data. To avoid this\nbehavior, set the `overwrite` argument of the init task to `False`.\n",
       "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core"
@@ -524,15 +532,14 @@
         "mem": 4000
       },
       "args_schema_non_parallel": {
-        "title": "CopyOmeZarrHcsPlate",
-        "type": "object",
+        "additionalProperties": false,
         "properties": {
           "zarr_urls": {
-            "title": "Zarr Urls",
-            "type": "array",
             "items": {
               "type": "string"
             },
+            "title": "Zarr Urls",
+            "type": "array",
             "description": "List of paths or urls to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
           },
           "zarr_dir": {
@@ -541,14 +548,14 @@
             "description": "path of the directory where the new OME-Zarrs will be created. (standard argument for Fractal tasks, managed by Fractal server)."
           },
           "suffix": {
-            "title": "Suffix",
             "default": "mip",
+            "title": "Suffix",
             "type": "string",
             "description": "The suffix that is used to transform `plate.zarr` into `plate_suffix.zarr`. Note that `None` is not currently supported."
           },
           "overwrite": {
-            "title": "Overwrite",
             "default": false,
+            "title": "Overwrite",
             "type": "boolean",
             "description": "If `True`, overwrite the task output."
           }
@@ -557,11 +564,27 @@
           "zarr_urls",
           "zarr_dir"
         ],
-        "additionalProperties": false
+        "type": "object",
+        "title": "CopyOmeZarrHcsPlate"
       },
       "args_schema_parallel": {
-        "title": "MaximumIntensityProjection",
-        "type": "object",
+        "$defs": {
+          "InitArgsMIP": {
+            "description": "Init Args for MIP task.",
+            "properties": {
+              "origin_url": {
+                "title": "Origin Url",
+                "type": "string"
+              }
+            },
+            "required": [
+              "origin_url"
+            ],
+            "title": "InitArgsMIP",
+            "type": "object"
+          }
+        },
+        "additionalProperties": false,
         "properties": {
           "zarr_url": {
             "title": "Zarr Url",
@@ -569,13 +592,13 @@
             "description": "Path or url to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
           },
           "init_args": {
-            "$ref": "#/definitions/InitArgsMIP",
-            "title": "Init_Args",
+            "$ref": "#/$defs/InitArgsMIP",
+            "title": "Init Args",
             "description": "Intialization arguments provided by `create_cellvoyager_ome_zarr_init`."
           },
           "overwrite": {
-            "title": "Overwrite",
             "default": false,
+            "title": "Overwrite",
             "type": "boolean",
             "description": "If `True`, overwrite the task output."
           }
@@ -584,23 +607,8 @@
           "zarr_url",
           "init_args"
         ],
-        "additionalProperties": false,
-        "definitions": {
-          "InitArgsMIP": {
-            "title": "InitArgsMIP",
-            "description": "Init Args for MIP task.",
-            "type": "object",
-            "properties": {
-              "origin_url": {
-                "title": "Origin Url",
-                "type": "string"
-              }
-            },
-            "required": [
-              "origin_url"
-            ]
-          }
-        }
+        "type": "object",
+        "title": "MaximumIntensityProjection"
       },
       "docs_info": "## copy_ome_zarr_hcs_plate\nDuplicate the OME-Zarr HCS structure for a set of zarr_urls.\n\nThis task only processes the zarr images in the zarr_urls, not all the\nimages in the plate. It copies all the  plate & well structure, but none\nof the image metadata or the actual image data:\n\n- For each plate, create a new OME-Zarr HCS plate with the attributes for\n    all the images in zarr_urls\n- For each well (in each plate), create a new zarr subgroup with the\n   same attributes as the original one.\n\nNote: this task makes use of methods from the `Attributes` class, see\nhttps://zarr.readthedocs.io/en/stable/api/attrs.html.\n## maximum_intensity_projection\nPerform maximum-intensity projection along Z axis.\n\nNote: this task stores the output in a new zarr file.\n",
       "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core"
@@ -619,8 +627,7 @@
         "mem": 4000
       },
       "args_schema_parallel": {
-        "title": "IlluminationCorrection",
-        "type": "object",
+        "additionalProperties": false,
         "properties": {
           "zarr_url": {
             "title": "Zarr Url",
@@ -633,207 +640,61 @@
             "description": "Path of folder of illumination profiles."
           },
           "illumination_profiles": {
-            "title": "Illumination Profiles",
-            "type": "object",
             "additionalProperties": {
               "type": "string"
             },
-            "description": "Dictionary where keys match the `wavelength_id` attributes of existing channels (e.g. `A01_C01` ) and values are the filenames of the corresponding illumination profiles."
-          },
-          "background": {
-            "title": "Background",
-            "default": 0,
-            "type": "integer",
-            "description": "Background value that is subtracted from the image before the illumination correction is applied. Set it to `0` if you don't want any background subtraction."
-          },
-          "input_ROI_table": {
-            "title": "Input Roi Table",
-            "default": "FOV_ROI_table",
-            "type": "string",
-            "description": "Name of the ROI table that contains the information about the location of the individual field of views (FOVs) to which the illumination correction shall be applied. Defaults to \"FOV_ROI_table\", the default name Fractal converters give the ROI tables that list all FOVs separately. If you generated your OME-Zarr with a different converter and used Import OME-Zarr to generate the ROI tables, `image_ROI_table` is the right choice if you only have 1 FOV per Zarr image and `grid_ROI_table` if you have multiple FOVs per Zarr image and set the right grid options during import."
-          },
-          "overwrite_input": {
-            "title": "Overwrite Input",
-            "default": true,
-            "type": "boolean",
-            "description": "If `True`, the results of this task will overwrite the input image data. If false, a new image is generated and the illumination corrected data is saved there."
-          },
-          "suffix": {
-            "title": "Suffix",
-            "default": "_illum_corr",
-            "type": "string",
-            "description": "What suffix to append to the illumination corrected images. Only relevant if `overwrite_input=False`."
-          }
-        },
-        "required": [
-          "zarr_url",
-          "illumination_profiles_folder",
-          "illumination_profiles"
-        ],
-        "additionalProperties": false
-      },
-      "docs_info": "## illumination_correction\nApplies illumination correction to the images in the OME-Zarr.\n\nAssumes that the illumination correction profiles were generated before\nseparately and that the same background subtraction was used during\ncalculation of the illumination correction (otherwise, it will not work\nwell & the correction may only be partial).\n",
-      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core"
-    },
-    {
-      "name": "Cellpose Segmentation",
-      "executable_parallel": "tasks/cellpose_segmentation.py",
-      "meta_parallel": {
-        "cpus_per_task": 4,
-        "mem": 16000,
-        "needs_gpu": true
-      },
-      "args_schema_parallel": {
-        "title": "CellposeSegmentation",
-        "type": "object",
-        "properties": {
-          "zarr_url": {
-            "title": "Zarr Url",
-            "type": "string",
-            "description": "Path or url to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "level": {
-            "title": "Level",
-            "type": "integer",
-            "description": "Pyramid level of the image to be segmented. Choose `0` to process at full resolution."
-          },
-          "channel": {
-            "$ref": "#/definitions/CellposeChannel1InputModel",
-            "title": "Channel",
-            "description": "Primary channel for segmentation; requires either `wavelength_id` (e.g. `A01_C01`) or `label` (e.g. `DAPI`), but not both. Also contains normalization options. By default, data is normalized so 0.0=1st percentile and 1.0=99th percentile of image intensities in each channel. This automatic normalization can lead to issues when the image to be segmented is very sparse. You can turn off the default rescaling. With the \"custom\" option, you can either provide your own rescaling percentiles or fixed rescaling upper and lower bound integers."
-          },
-          "channel2": {
-            "$ref": "#/definitions/CellposeChannel2InputModel",
-            "title": "Channel2",
-            "description": "Second channel for segmentation (in the same format as `channel`). If specified, cellpose runs in dual channel mode. For dual channel segmentation of cells, the first channel should contain the membrane marker, the second channel should contain the nuclear marker."
-          },
-          "input_ROI_table": {
-            "title": "Input Roi Table",
-            "default": "FOV_ROI_table",
-            "type": "string",
-            "description": "Name of the ROI table over which the task loops to apply Cellpose segmentation. Examples: `FOV_ROI_table` => loop over the field of views, `organoid_ROI_table` => loop over the organoid ROI table (generated by another task), `well_ROI_table` => process the whole well as one image."
-          },
-          "output_ROI_table": {
-            "title": "Output Roi Table",
-            "type": "string",
-            "description": "If provided, a ROI table with that name is created, which will contain the bounding boxes of the newly segmented labels. ROI tables should have `ROI` in their name."
-          },
-          "output_label_name": {
-            "title": "Output Label Name",
-            "type": "string",
-            "description": "Name of the output label image (e.g. `\"organoids\"`)."
-          },
-          "diameter_level0": {
-            "title": "Diameter Level0",
-            "default": 30.0,
-            "type": "number",
-            "description": "Expected diameter of the objects that should be segmented in pixels at level 0. Initial diameter is rescaled using the `level` that was selected. The rescaled value is passed as the diameter to the `CellposeModel.eval` method."
-          },
-          "model_type": {
-            "title": "Model Type",
-            "default": "cyto2",
-            "enum": [
-              "cyto",
-              "nuclei",
-              "tissuenet",
-              "livecell",
-              "cyto2",
-              "general",
-              "CP",
-              "CPx",
-              "TN1",
-              "TN2",
-              "TN3",
-              "LC1",
-              "LC2",
-              "LC3",
-              "LC4"
-            ],
-            "type": "string",
-            "description": "Parameter of `CellposeModel` class. Defines which model should be used. Typical choices are `nuclei`, `cyto`, `cyto2`, etc."
-          },
-          "pretrained_model": {
-            "title": "Pretrained Model",
-            "type": "string",
-            "description": "Parameter of `CellposeModel` class (takes precedence over `model_type`). Allows you to specify the path of a custom trained cellpose model."
-          },
-          "relabeling": {
-            "title": "Relabeling",
-            "default": true,
-            "type": "boolean",
-            "description": "If `True`, apply relabeling so that label values are unique for all objects in the well."
-          },
-          "use_masks": {
-            "title": "Use Masks",
-            "default": true,
-            "type": "boolean",
-            "description": "If `True`, try to use masked loading and fall back to `use_masks=False` if the ROI table is not suitable. Masked loading is relevant when only a subset of the bounding box should actually be processed (e.g. running within `organoid_ROI_table`)."
-          },
-          "advanced_cellpose_model_params": {
-            "$ref": "#/definitions/CellposeModelParams",
-            "title": "Advanced_Cellpose_Model_Params",
-            "description": "Advanced Cellpose model parameters that are passed to the Cellpose `model.eval` method."
-          },
-          "overwrite": {
-            "title": "Overwrite",
-            "default": true,
-            "type": "boolean",
-            "description": "If `True`, overwrite the task output."
-          }
-        },
-        "required": [
-          "zarr_url",
-          "level",
-          "channel"
-        ],
-        "additionalProperties": false,
-        "definitions": {
-          "CellposeCustomNormalizer": {
-            "title": "CellposeCustomNormalizer",
-            "description": "Validator to handle different normalization scenarios for Cellpose models",
-            "type": "object",
-            "properties": {
-              "type": {
-                "title": "Type",
-                "default": "default",
-                "enum": [
-                  "default",
-                  "custom",
-                  "no_normalization"
-                ],
-                "type": "string",
-                "description": "One of `default` (Cellpose default normalization), `custom` (using the other custom parameters) or `no_normalization`."
-              },
-              "lower_percentile": {
-                "title": "Lower Percentile",
-                "minimum": 0,
-                "maximum": 100,
-                "type": "number",
-                "description": "Specify a custom lower-bound percentile for rescaling as a float value between 0 and 100. Set to 1 to run the same as default). You can only specify percentiles or bounds, not both."
-              },
-              "upper_percentile": {
-                "title": "Upper Percentile",
-                "minimum": 0,
-                "maximum": 100,
-                "type": "number",
-                "description": "Specify a custom upper-bound percentile for rescaling as a float value between 0 and 100. Set to 99 to run the same as default, set to e.g. 99.99 if the default rescaling was too harsh. You can only specify percentiles or bounds, not both."
-              },
-              "lower_bound": {
-                "title": "Lower Bound",
-                "type": "integer",
-                "description": "Explicit lower bound value to rescale the image at. Needs to be an integer, e.g. 100. You can only specify percentiles or bounds, not both."
-              },
-              "upper_bound": {
-                "title": "Upper Bound",
-                "type": "integer",
-                "description": "Explicit upper bound value to rescale the image at. Needs to be an integer, e.g. 2000. You can only specify percentiles or bounds, not both."
-              }
-            }
+            "title": "Illumination Profiles",
+            "type": "object",
+            "description": "Dictionary where keys match the `wavelength_id` attributes of existing channels (e.g. `A01_C01` ) and values are the filenames of the corresponding illumination profiles."
+          },
+          "background": {
+            "default": 0,
+            "title": "Background",
+            "type": "integer",
+            "description": "Background value that is subtracted from the image before the illumination correction is applied. Set it to `0` if you don't want any background subtraction."
+          },
+          "input_ROI_table": {
+            "default": "FOV_ROI_table",
+            "title": "Input Roi Table",
+            "type": "string",
+            "description": "Name of the ROI table that contains the information about the location of the individual field of views (FOVs) to which the illumination correction shall be applied. Defaults to \"FOV_ROI_table\", the default name Fractal converters give the ROI tables that list all FOVs separately. If you generated your OME-Zarr with a different converter and used Import OME-Zarr to generate the ROI tables, `image_ROI_table` is the right choice if you only have 1 FOV per Zarr image and `grid_ROI_table` if you have multiple FOVs per Zarr image and set the right grid options during import."
+          },
+          "overwrite_input": {
+            "default": true,
+            "title": "Overwrite Input",
+            "type": "boolean",
+            "description": "If `True`, the results of this task will overwrite the input image data. If false, a new image is generated and the illumination corrected data is saved there."
           },
+          "suffix": {
+            "default": "_illum_corr",
+            "title": "Suffix",
+            "type": "string",
+            "description": "What suffix to append to the illumination corrected images. Only relevant if `overwrite_input=False`."
+          }
+        },
+        "required": [
+          "zarr_url",
+          "illumination_profiles_folder",
+          "illumination_profiles"
+        ],
+        "type": "object",
+        "title": "IlluminationCorrection"
+      },
+      "docs_info": "## illumination_correction\nApplies illumination correction to the images in the OME-Zarr.\n\nAssumes that the illumination correction profiles were generated before\nseparately and that the same background subtraction was used during\ncalculation of the illumination correction (otherwise, it will not work\nwell & the correction may only be partial).\n",
+      "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core"
+    },
+    {
+      "name": "Cellpose Segmentation",
+      "executable_parallel": "tasks/cellpose_segmentation.py",
+      "meta_parallel": {
+        "cpus_per_task": 4,
+        "mem": 16000,
+        "needs_gpu": true
+      },
+      "args_schema_parallel": {
+        "$defs": {
           "CellposeChannel1InputModel": {
-            "title": "CellposeChannel1InputModel",
             "description": "Channel input for cellpose with normalization options.",
-            "type": "object",
             "properties": {
               "wavelength_id": {
                 "title": "Wavelength Id",
@@ -846,16 +707,16 @@
                 "description": "Name of the channel. Can only be specified if wavelength_id is not set."
               },
               "normalize": {
-                "$ref": "#/definitions/CellposeCustomNormalizer",
+                "$ref": "#/$defs/CellposeCustomNormalizer",
                 "title": "Normalize",
                 "description": "Validator to handle different normalization scenarios for Cellpose models"
               }
-            }
+            },
+            "title": "CellposeChannel1InputModel",
+            "type": "object"
           },
           "CellposeChannel2InputModel": {
-            "title": "CellposeChannel2InputModel",
             "description": "Channel input for secondary cellpose channel with normalization options.",
-            "type": "object",
             "properties": {
               "wavelength_id": {
                 "title": "Wavelength Id",
@@ -868,26 +729,68 @@
                 "description": "Name of the channel. Can only be specified if wavelength_id is not set."
               },
               "normalize": {
-                "$ref": "#/definitions/CellposeCustomNormalizer",
+                "$ref": "#/$defs/CellposeCustomNormalizer",
                 "title": "Normalize",
                 "description": "Validator to handle different normalization scenarios for Cellpose models"
               }
-            }
+            },
+            "title": "CellposeChannel2InputModel",
+            "type": "object"
+          },
+          "CellposeCustomNormalizer": {
+            "description": "Validator to handle different normalization scenarios for Cellpose models",
+            "properties": {
+              "type": {
+                "default": "default",
+                "enum": [
+                  "default",
+                  "custom",
+                  "no_normalization"
+                ],
+                "title": "Type",
+                "type": "string",
+                "description": "One of `default` (Cellpose default normalization), `custom` (using the other custom parameters) or `no_normalization`."
+              },
+              "lower_percentile": {
+                "maximum": 100.0,
+                "minimum": 0.0,
+                "title": "Lower Percentile",
+                "type": "number",
+                "description": "Specify a custom lower-bound percentile for rescaling as a float value between 0 and 100. Set to 1 to run the same as default). You can only specify percentiles or bounds, not both."
+              },
+              "upper_percentile": {
+                "maximum": 100.0,
+                "minimum": 0.0,
+                "title": "Upper Percentile",
+                "type": "number",
+                "description": "Specify a custom upper-bound percentile for rescaling as a float value between 0 and 100. Set to 99 to run the same as default, set to e.g. 99.99 if the default rescaling was too harsh. You can only specify percentiles or bounds, not both."
+              },
+              "lower_bound": {
+                "title": "Lower Bound",
+                "type": "integer",
+                "description": "Explicit lower bound value to rescale the image at. Needs to be an integer, e.g. 100. You can only specify percentiles or bounds, not both."
+              },
+              "upper_bound": {
+                "title": "Upper Bound",
+                "type": "integer",
+                "description": "Explicit upper bound value to rescale the image at. Needs to be an integer, e.g. 2000. You can only specify percentiles or bounds, not both."
+              }
+            },
+            "title": "CellposeCustomNormalizer",
+            "type": "object"
           },
           "CellposeModelParams": {
-            "title": "CellposeModelParams",
             "description": "Advanced Cellpose Model Parameters",
-            "type": "object",
             "properties": {
               "cellprob_threshold": {
-                "title": "Cellprob Threshold",
                 "default": 0.0,
+                "title": "Cellprob Threshold",
                 "type": "number",
                 "description": "Parameter of `CellposeModel.eval` method. Valid values between -6 to 6. From Cellpose documentation: \"Decrease this threshold if cellpose is not returning as many ROIs as you'd expect. Similarly, increase this threshold if cellpose is returning too ROIs particularly from dim areas.\""
               },
               "flow_threshold": {
-                "title": "Flow Threshold",
                 "default": 0.4,
+                "title": "Flow Threshold",
                 "type": "number",
                 "description": "Parameter of `CellposeModel.eval` method. Valid values between 0.0 and 1.0. From Cellpose documentation: \"Increase this threshold if cellpose is not returning as many ROIs as you'd expect. Similarly, decrease this threshold if cellpose is returning too many ill-shaped ROIs.\""
               },
@@ -897,74 +800,179 @@
                 "description": "Ratio of the pixel sizes along Z and XY axis (ignored if the image is not three-dimensional). If unset, it is inferred from the OME-NGFF metadata."
               },
               "min_size": {
-                "title": "Min Size",
                 "default": 15,
+                "title": "Min Size",
                 "type": "integer",
                 "description": "Parameter of `CellposeModel` class. Minimum size of the segmented objects (in pixels). Use `-1` to turn off the size filter."
               },
               "augment": {
-                "title": "Augment",
                 "default": false,
+                "title": "Augment",
                 "type": "boolean",
                 "description": "Parameter of `CellposeModel` class. Whether to use cellpose augmentation to tile images with overlap."
               },
               "net_avg": {
-                "title": "Net Avg",
                 "default": false,
+                "title": "Net Avg",
                 "type": "boolean",
                 "description": "Parameter of `CellposeModel` class. Whether to use cellpose net averaging to run the 4 built-in networks (useful for `nuclei`, `cyto` and `cyto2`, not sure it works for the others)."
               },
               "use_gpu": {
-                "title": "Use Gpu",
                 "default": true,
+                "title": "Use Gpu",
                 "type": "boolean",
                 "description": "If `False`, always use the CPU; if `True`, use the GPU if possible (as defined in `cellpose.core.use_gpu()`) and fall-back to the CPU otherwise."
               },
               "batch_size": {
-                "title": "Batch Size",
                 "default": 8,
+                "title": "Batch Size",
                 "type": "integer",
                 "description": "number of 224x224 patches to run simultaneously on the GPU (can make smaller or bigger depending on GPU memory usage)"
               },
               "invert": {
-                "title": "Invert",
                 "default": false,
+                "title": "Invert",
                 "type": "boolean",
                 "description": "invert image pixel intensity before running network (if True, image is also normalized)"
               },
               "tile": {
-                "title": "Tile",
                 "default": true,
+                "title": "Tile",
                 "type": "boolean",
                 "description": "tiles image to ensure GPU/CPU memory usage limited (recommended)"
               },
               "tile_overlap": {
-                "title": "Tile Overlap",
                 "default": 0.1,
+                "title": "Tile Overlap",
                 "type": "number",
                 "description": "fraction of overlap of tiles when computing flows"
               },
               "resample": {
-                "title": "Resample",
                 "default": true,
+                "title": "Resample",
                 "type": "boolean",
                 "description": "run dynamics at original image size (will be slower but create more accurate boundaries)"
               },
               "interp": {
-                "title": "Interp",
                 "default": true,
+                "title": "Interp",
                 "type": "boolean",
                 "description": "interpolate during 2D dynamics (not available in 3D) (in previous versions it was False, now it defaults to True)"
               },
               "stitch_threshold": {
-                "title": "Stitch Threshold",
                 "default": 0.0,
+                "title": "Stitch Threshold",
                 "type": "number",
                 "description": "if stitch_threshold>0.0 and not do_3D and equal image sizes, masks are stitched in 3D to return volume segmentation"
               }
-            }
+            },
+            "title": "CellposeModelParams",
+            "type": "object"
+          }
+        },
+        "additionalProperties": false,
+        "properties": {
+          "zarr_url": {
+            "title": "Zarr Url",
+            "type": "string",
+            "description": "Path or url to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
+          },
+          "level": {
+            "title": "Level",
+            "type": "integer",
+            "description": "Pyramid level of the image to be segmented. Choose `0` to process at full resolution."
+          },
+          "channel": {
+            "$ref": "#/$defs/CellposeChannel1InputModel",
+            "title": "Channel",
+            "description": "Primary channel for segmentation; requires either `wavelength_id` (e.g. `A01_C01`) or `label` (e.g. `DAPI`), but not both. Also contains normalization options. By default, data is normalized so 0.0=1st percentile and 1.0=99th percentile of image intensities in each channel. This automatic normalization can lead to issues when the image to be segmented is very sparse. You can turn off the default rescaling. With the \"custom\" option, you can either provide your own rescaling percentiles or fixed rescaling upper and lower bound integers."
+          },
+          "channel2": {
+            "$ref": "#/$defs/CellposeChannel2InputModel",
+            "title": "Channel2",
+            "description": "Second channel for segmentation (in the same format as `channel`). If specified, cellpose runs in dual channel mode. For dual channel segmentation of cells, the first channel should contain the membrane marker, the second channel should contain the nuclear marker."
+          },
+          "input_ROI_table": {
+            "default": "FOV_ROI_table",
+            "title": "Input Roi Table",
+            "type": "string",
+            "description": "Name of the ROI table over which the task loops to apply Cellpose segmentation. Examples: `FOV_ROI_table` => loop over the field of views, `organoid_ROI_table` => loop over the organoid ROI table (generated by another task), `well_ROI_table` => process the whole well as one image."
+          },
+          "output_ROI_table": {
+            "title": "Output Roi Table",
+            "type": "string",
+            "description": "If provided, a ROI table with that name is created, which will contain the bounding boxes of the newly segmented labels. ROI tables should have `ROI` in their name."
+          },
+          "output_label_name": {
+            "title": "Output Label Name",
+            "type": "string",
+            "description": "Name of the output label image (e.g. `\"organoids\"`)."
+          },
+          "diameter_level0": {
+            "default": 30.0,
+            "title": "Diameter Level0",
+            "type": "number",
+            "description": "Expected diameter of the objects that should be segmented in pixels at level 0. Initial diameter is rescaled using the `level` that was selected. The rescaled value is passed as the diameter to the `CellposeModel.eval` method."
+          },
+          "model_type": {
+            "default": "cyto2",
+            "enum": [
+              "cyto",
+              "nuclei",
+              "tissuenet",
+              "livecell",
+              "cyto2",
+              "general",
+              "CP",
+              "CPx",
+              "TN1",
+              "TN2",
+              "TN3",
+              "LC1",
+              "LC2",
+              "LC3",
+              "LC4"
+            ],
+            "title": "Model Type",
+            "type": "string",
+            "description": "Parameter of `CellposeModel` class. Defines which model should be used. Typical choices are `nuclei`, `cyto`, `cyto2`, etc."
+          },
+          "pretrained_model": {
+            "title": "Pretrained Model",
+            "type": "string",
+            "description": "Parameter of `CellposeModel` class (takes precedence over `model_type`). Allows you to specify the path of a custom trained cellpose model."
+          },
+          "relabeling": {
+            "default": true,
+            "title": "Relabeling",
+            "type": "boolean",
+            "description": "If `True`, apply relabeling so that label values are unique for all objects in the well."
+          },
+          "use_masks": {
+            "default": true,
+            "title": "Use Masks",
+            "type": "boolean",
+            "description": "If `True`, try to use masked loading and fall back to `use_masks=False` if the ROI table is not suitable. Masked loading is relevant when only a subset of the bounding box should actually be processed (e.g. running within `organoid_ROI_table`)."
+          },
+          "advanced_cellpose_model_params": {
+            "$ref": "#/$defs/CellposeModelParams",
+            "title": "Advanced Cellpose Model Params",
+            "description": "Advanced Cellpose model parameters that are passed to the Cellpose `model.eval` method."
+          },
+          "overwrite": {
+            "default": true,
+            "title": "Overwrite",
+            "type": "boolean",
+            "description": "If `True`, overwrite the task output."
           }
-        }
+        },
+        "required": [
+          "zarr_url",
+          "level",
+          "channel"
+        ],
+        "type": "object",
+        "title": "CellposeSegmentation"
       },
       "docs_info": "## cellpose_segmentation\nRun cellpose segmentation on the ROIs of a single OME-Zarr image.\n",
       "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core"
@@ -982,15 +990,14 @@
         "mem": 8000
       },
       "args_schema_non_parallel": {
-        "title": "ImageBasedRegistrationHcsInit",
-        "type": "object",
+        "additionalProperties": false,
         "properties": {
           "zarr_urls": {
-            "title": "Zarr Urls",
-            "type": "array",
             "items": {
               "type": "string"
             },
+            "title": "Zarr Urls",
+            "type": "array",
             "description": "List of paths or urls to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
           },
           "zarr_dir": {
@@ -999,21 +1006,46 @@
             "description": "path of the directory where the new OME-Zarrs will be created. Not used by this task. (standard argument for Fractal tasks, managed by Fractal server)."
           },
           "reference_acquisition": {
-            "title": "Reference Acquisition",
             "default": 0,
+            "title": "Reference Acquisition",
             "type": "integer",
             "description": "Which acquisition to register against. Needs to match the acquisition metadata in the OME-Zarr image."
           }
         },
-        "required": [
-          "zarr_urls",
-          "zarr_dir"
-        ],
-        "additionalProperties": false
-      },
-      "args_schema_parallel": {
-        "title": "CalculateRegistrationImageBased",
-        "type": "object",
+        "required": [
+          "zarr_urls",
+          "zarr_dir"
+        ],
+        "type": "object",
+        "title": "ImageBasedRegistrationHcsInit"
+      },
+      "args_schema_parallel": {
+        "$defs": {
+          "InitArgsRegistration": {
+            "description": "Registration init args.",
+            "properties": {
+              "reference_zarr_url": {
+                "title": "Reference Zarr Url",
+                "type": "string"
+              }
+            },
+            "required": [
+              "reference_zarr_url"
+            ],
+            "title": "InitArgsRegistration",
+            "type": "object"
+          },
+          "RegistrationMethod": {
+            "description": "RegistrationMethod Enum class",
+            "enum": [
+              "phase_cross_correlation",
+              "chi2_shift"
+            ],
+            "title": "RegistrationMethod",
+            "type": "string"
+          }
+        },
+        "additionalProperties": false,
         "properties": {
           "zarr_url": {
             "title": "Zarr Url",
@@ -1021,8 +1053,8 @@
             "description": "Path or url to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
           },
           "init_args": {
-            "$ref": "#/definitions/InitArgsRegistration",
-            "title": "Init_Args",
+            "$ref": "#/$defs/InitArgsRegistration",
+            "title": "Init Args",
             "description": "Intialization arguments provided by `image_based_registration_hcs_init`. They contain the reference_zarr_url that is used for registration. (standard argument for Fractal tasks, managed by Fractal server)."
           },
           "wavelength_id": {
@@ -1031,24 +1063,24 @@
             "description": "Wavelength that will be used for image-based registration; e.g. `A01_C01` for Yokogawa, `C01` for MD."
           },
           "method": {
-            "default": "phase_cross_correlation",
             "allOf": [
               {
-                "$ref": "#/definitions/RegistrationMethod"
+                "$ref": "#/$defs/RegistrationMethod"
               }
             ],
+            "default": "phase_cross_correlation",
             "title": "Method",
             "description": "Method to use for image registration. The available methods are `phase_cross_correlation` (scikit-image package, works for 2D & 3D) and \"chi2_shift\" (image_registration package, only works for 2D images)."
           },
           "roi_table": {
-            "title": "Roi Table",
             "default": "FOV_ROI_table",
+            "title": "Roi Table",
             "type": "string",
             "description": "Name of the ROI table over which the task loops to calculate the registration. Examples: `FOV_ROI_table` => loop over the field of views, `well_ROI_table` => process the whole well as one image."
           },
           "level": {
-            "title": "Level",
             "default": 2,
+            "title": "Level",
             "type": "integer",
             "description": "Pyramid level of the image to be used for registration. Choose `0` to process at full resolution."
           }
@@ -1058,31 +1090,8 @@
           "init_args",
           "wavelength_id"
         ],
-        "additionalProperties": false,
-        "definitions": {
-          "InitArgsRegistration": {
-            "title": "InitArgsRegistration",
-            "description": "Registration init args.",
-            "type": "object",
-            "properties": {
-              "reference_zarr_url": {
-                "title": "Reference Zarr Url",
-                "type": "string"
-              }
-            },
-            "required": [
-              "reference_zarr_url"
-            ]
-          },
-          "RegistrationMethod": {
-            "title": "RegistrationMethod",
-            "description": "RegistrationMethod Enum class",
-            "enum": [
-              "phase_cross_correlation",
-              "chi2_shift"
-            ]
-          }
-        }
+        "type": "object",
+        "title": "CalculateRegistrationImageBased"
       },
       "docs_info": "## image_based_registration_hcs_init\nInitialized calculate registration task\n\nThis task prepares a parallelization list of all zarr_urls that need to be\nused to calculate the registration between acquisitions (all zarr_urls\nexcept the reference acquisition vs. the reference acquisition).\nThis task only works for HCS OME-Zarrs for 2 reasons: Only HCS OME-Zarrs\ncurrently have defined acquisition metadata to determine reference\nacquisitions. And we have only implemented the grouping of images for\nHCS OME-Zarrs by well (with the assumption that every well just has 1\nimage per acqusition).\n## calculate_registration_image_based\nCalculate registration based on images\n\nThis task consists of 3 parts:\n\n1. Loading the images of a given ROI (=> loop over ROIs)\n2. Calculating the transformation for that ROI\n3. Storing the calculated transformation in the ROI table\n",
       "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core"
@@ -1100,15 +1109,14 @@
         "mem": 1000
       },
       "args_schema_non_parallel": {
-        "title": "InitGroupByWellForMultiplexing",
-        "type": "object",
+        "additionalProperties": false,
         "properties": {
           "zarr_urls": {
-            "title": "Zarr Urls",
-            "type": "array",
             "items": {
               "type": "string"
             },
+            "title": "Zarr Urls",
+            "type": "array",
             "description": "List of paths or urls to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
           },
           "zarr_dir": {
@@ -1117,8 +1125,8 @@
             "description": "path of the directory where the new OME-Zarrs will be created. Not used by this task. (standard argument for Fractal tasks, managed by Fractal server)."
           },
           "reference_acquisition": {
-            "title": "Reference Acquisition",
             "default": 0,
+            "title": "Reference Acquisition",
             "type": "integer",
             "description": "Which acquisition to register against. Uses the OME-NGFF HCS well metadata acquisition keys to find the reference acquisition."
           }
@@ -1127,11 +1135,30 @@
           "zarr_urls",
           "zarr_dir"
         ],
-        "additionalProperties": false
+        "type": "object",
+        "title": "InitGroupByWellForMultiplexing"
       },
       "args_schema_parallel": {
-        "title": "FindRegistrationConsensus",
-        "type": "object",
+        "$defs": {
+          "InitArgsRegistrationConsensus": {
+            "description": "Registration consensus init args.",
+            "properties": {
+              "zarr_url_list": {
+                "items": {
+                  "type": "string"
+                },
+                "title": "Zarr Url List",
+                "type": "array"
+              }
+            },
+            "required": [
+              "zarr_url_list"
+            ],
+            "title": "InitArgsRegistrationConsensus",
+            "type": "object"
+          }
+        },
+        "additionalProperties": false,
         "properties": {
           "zarr_url": {
             "title": "Zarr Url",
@@ -1139,13 +1166,13 @@
             "description": "Path or url to the individual OME-Zarr image to be processed. Refers to the zarr_url of the reference acquisition. (standard argument for Fractal tasks, managed by Fractal server)."
           },
           "init_args": {
-            "$ref": "#/definitions/InitArgsRegistrationConsensus",
-            "title": "Init_Args",
+            "$ref": "#/$defs/InitArgsRegistrationConsensus",
+            "title": "Init Args",
             "description": "Intialization arguments provided by `init_group_by_well_for_multiplexing`. It contains the zarr_url_list listing all the zarr_urls in the same well as the zarr_url of the reference acquisition that are being processed. (standard argument for Fractal tasks, managed by Fractal server)."
           },
           "roi_table": {
-            "title": "Roi Table",
             "default": "FOV_ROI_table",
+            "title": "Roi Table",
             "type": "string",
             "description": "Name of the ROI table over which the task loops to calculate the registration. Examples: `FOV_ROI_table` => loop over the field of views, `well_ROI_table` => process the whole well as one image."
           },
@@ -1159,26 +1186,8 @@
           "zarr_url",
           "init_args"
         ],
-        "additionalProperties": false,
-        "definitions": {
-          "InitArgsRegistrationConsensus": {
-            "title": "InitArgsRegistrationConsensus",
-            "description": "Registration consensus init args.",
-            "type": "object",
-            "properties": {
-              "zarr_url_list": {
-                "title": "Zarr Url List",
-                "type": "array",
-                "items": {
-                  "type": "string"
-                }
-              }
-            },
-            "required": [
-              "zarr_url_list"
-            ]
-          }
-        }
+        "type": "object",
+        "title": "FindRegistrationConsensus"
       },
       "docs_info": "## init_group_by_well_for_multiplexing\nFinds images for all acquisitions per well.\n\nReturns the parallelization_list to run `find_registration_consensus`.\n## find_registration_consensus\nApplies pre-calculated registration to ROI tables.\n\nApply pre-calculated registration such that resulting ROIs contain\nthe consensus align region between all acquisitions.\n\nParallelization level: well\n",
       "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core"
@@ -1197,8 +1206,7 @@
         "mem": 4000
       },
       "args_schema_parallel": {
-        "title": "ApplyRegistrationToImage",
-        "type": "object",
+        "additionalProperties": false,
         "properties": {
           "zarr_url": {
             "title": "Zarr Url",
@@ -1211,14 +1219,14 @@
             "description": "Name of the ROI table which has been registered and will be applied to mask and shift the images. Examples: `registered_FOV_ROI_table` => loop over the field of views, `registered_well_ROI_table` => process the whole well as one image."
           },
           "reference_acquisition": {
-            "title": "Reference Acquisition",
             "default": 0,
+            "title": "Reference Acquisition",
             "type": "integer",
             "description": "Which acquisition to register against. Uses the OME-NGFF HCS well metadata acquisition keys to find the reference acquisition."
           },
           "overwrite_input": {
-            "title": "Overwrite Input",
             "default": true,
+            "title": "Overwrite Input",
             "type": "boolean",
             "description": "Whether the old image data should be replaced with the newly registered image data. Currently only implemented for `overwrite_input=True`."
           }
@@ -1227,7 +1235,8 @@
           "zarr_url",
           "registered_roi_table"
         ],
-        "additionalProperties": false
+        "type": "object",
+        "title": "ApplyRegistrationToImage"
       },
       "docs_info": "## apply_registration_to_image\nApply registration to images by using a registered ROI table\n\nThis task consists of 4 parts:\n\n1. Mask all regions in images that are not available in the\nregistered ROI table and store each acquisition aligned to the\nreference_acquisition (by looping over ROIs).\n2. Do the same for all label images.\n3. Copy all tables from the non-aligned image to the aligned image\n(currently only works well if the only tables are well & FOV ROI tables\n(registered and original). Not implemented for measurement tables and\nother ROI tables).\n4. Clean up: Delete the old, non-aligned image and rename the new,\naligned image to take over its place.\n",
       "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core"
@@ -1236,15 +1245,14 @@
       "name": "Import OME-Zarr",
       "executable_non_parallel": "tasks/import_ome_zarr.py",
       "args_schema_non_parallel": {
-        "title": "ImportOmeZarr",
-        "type": "object",
+        "additionalProperties": false,
         "properties": {
           "zarr_urls": {
-            "title": "Zarr Urls",
-            "type": "array",
             "items": {
               "type": "string"
             },
+            "title": "Zarr Urls",
+            "type": "array",
             "description": "List of paths or urls to the individual OME-Zarr image to be processed. Not used. (standard argument for Fractal tasks, managed by Fractal server)."
           },
           "zarr_dir": {
@@ -1258,38 +1266,38 @@
             "description": "The OME-Zarr name, without its parent folder. The parent folder is provided by zarr_dir; e.g. `zarr_name=\"array.zarr\"`, if the OME-Zarr path is in `/zarr_dir/array.zarr`."
           },
           "update_omero_metadata": {
-            "title": "Update Omero Metadata",
             "default": true,
+            "title": "Update Omero Metadata",
             "type": "boolean",
             "description": "Whether to update Omero-channels metadata, to make them Fractal-compatible."
           },
           "add_image_ROI_table": {
-            "title": "Add Image Roi Table",
             "default": true,
+            "title": "Add Image Roi Table",
             "type": "boolean",
             "description": "Whether to add a `image_ROI_table` table to each image, with a single ROI covering the whole image."
           },
           "add_grid_ROI_table": {
-            "title": "Add Grid Roi Table",
             "default": true,
+            "title": "Add Grid Roi Table",
             "type": "boolean",
             "description": "Whether to add a `grid_ROI_table` table to each image, with the image split into a rectangular grid of ROIs."
           },
           "grid_y_shape": {
-            "title": "Grid Y Shape",
             "default": 2,
+            "title": "Grid Y Shape",
             "type": "integer",
             "description": "Y shape of the ROI grid in `grid_ROI_table`."
           },
           "grid_x_shape": {
-            "title": "Grid X Shape",
             "default": 2,
+            "title": "Grid X Shape",
             "type": "integer",
             "description": "X shape of the ROI grid in `grid_ROI_table`."
           },
           "overwrite": {
-            "title": "Overwrite",
             "default": false,
+            "title": "Overwrite",
             "type": "boolean",
             "description": "Whether new ROI tables (added when `add_image_ROI_table` and/or `add_grid_ROI_table` are `True`) can overwite existing ones."
           }
@@ -1299,7 +1307,8 @@
           "zarr_dir",
           "zarr_name"
         ],
-        "additionalProperties": false
+        "type": "object",
+        "title": "ImportOmeZarr"
       },
       "docs_info": "## import_ome_zarr\nImport a single OME-Zarr into Fractal.\n\nThe single OME-Zarr can be a full OME-Zarr HCS plate or an individual\nOME-Zarr image. The image needs to be in the zarr_dir as specified by the\ndataset. The current version of this task:\n\n1. Creates the appropriate components-related metadata, needed for\n   processing an existing OME-Zarr through Fractal.\n2. Optionally adds new ROI tables to the existing OME-Zarr.\n",
       "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core"
@@ -1312,78 +1321,9 @@
         "mem": 32000
       },
       "args_schema_parallel": {
-        "title": "NapariWorkflowsWrapper",
-        "type": "object",
-        "properties": {
-          "zarr_url": {
-            "title": "Zarr Url",
-            "type": "string",
-            "description": "Path or url to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
-          },
-          "workflow_file": {
-            "title": "Workflow File",
-            "type": "string",
-            "description": "Absolute path to napari-workflows YAML file"
-          },
-          "input_specs": {
-            "title": "Input Specs",
-            "type": "object",
-            "additionalProperties": {
-              "$ref": "#/definitions/NapariWorkflowsInput"
-            },
-            "description": "A dictionary of `NapariWorkflowsInput` values."
-          },
-          "output_specs": {
-            "title": "Output Specs",
-            "type": "object",
-            "additionalProperties": {
-              "$ref": "#/definitions/NapariWorkflowsOutput"
-            },
-            "description": "A dictionary of `NapariWorkflowsOutput` values."
-          },
-          "input_ROI_table": {
-            "title": "Input Roi Table",
-            "default": "FOV_ROI_table",
-            "type": "string",
-            "description": "Name of the ROI table over which the task loops to apply napari workflows. Examples: `FOV_ROI_table` => loop over the field of views; `organoid_ROI_table` => loop over the organoid ROI table (generated by another task); `well_ROI_table` => process the whole well as one image."
-          },
-          "level": {
-            "title": "Level",
-            "default": 0,
-            "type": "integer",
-            "description": "Pyramid level of the image to be used as input for napari-workflows. Choose `0` to process at full resolution. Levels > 0 are currently only supported for workflows that only have intensity images as input and only produce a label images as output."
-          },
-          "relabeling": {
-            "title": "Relabeling",
-            "default": true,
-            "type": "boolean",
-            "description": "If `True`, apply relabeling so that label values are unique across all ROIs in the well."
-          },
-          "expected_dimensions": {
-            "title": "Expected Dimensions",
-            "default": 3,
-            "type": "integer",
-            "description": "Expected dimensions (either `2` or `3`). Useful when loading 2D images that are stored in a 3D array with shape `(1, size_x, size_y)` [which is the default way Fractal stores 2D images], but you want to make sure the napari workflow gets a 2D array to process. Also useful to set to `2` when loading a 2D OME-Zarr that is saved as `(size_x, size_y)`."
-          },
-          "overwrite": {
-            "title": "Overwrite",
-            "default": true,
-            "type": "boolean",
-            "description": "If `True`, overwrite the task output."
-          }
-        },
-        "required": [
-          "zarr_url",
-          "workflow_file",
-          "input_specs",
-          "output_specs"
-        ],
-        "additionalProperties": false,
-        "definitions": {
+        "$defs": {
           "ChannelInputModel": {
-            "title": "ChannelInputModel",
             "description": "A channel which is specified by either `wavelength_id` or `label`.",
-            "type": "object",
             "properties": {
               "wavelength_id": {
                 "title": "Wavelength Id",
@@ -1395,19 +1335,19 @@
                 "type": "string",
                 "description": "Name of the channel. Can only be specified if wavelength_id is not set."
               }
-            }
+            },
+            "title": "ChannelInputModel",
+            "type": "object"
           },
           "NapariWorkflowsInput": {
-            "title": "NapariWorkflowsInput",
             "description": "A value of the `input_specs` argument in `napari_workflows_wrapper`.",
-            "type": "object",
             "properties": {
               "type": {
-                "title": "Type",
                 "enum": [
                   "image",
                   "label"
                 ],
+                "title": "Type",
                 "type": "string",
                 "description": "Input type (either `image` or `label`)."
               },
@@ -1417,26 +1357,30 @@
                 "description": "Label name (for label inputs only)."
               },
               "channel": {
-                "$ref": "#/definitions/ChannelInputModel",
+                "allOf": [
+                  {
+                    "$ref": "#/$defs/ChannelInputModel"
+                  }
+                ],
                 "title": "Channel",
                 "description": "`ChannelInputModel` object (for image inputs only)."
               }
             },
             "required": [
               "type"
-            ]
+            ],
+            "title": "NapariWorkflowsInput",
+            "type": "object"
           },
           "NapariWorkflowsOutput": {
-            "title": "NapariWorkflowsOutput",
             "description": "A value of the `output_specs` argument in `napari_workflows_wrapper`.",
-            "type": "object",
             "properties": {
               "type": {
-                "title": "Type",
                 "enum": [
                   "label",
                   "dataframe"
                 ],
+                "title": "Type",
                 "type": "string",
                 "description": "Output type (either `label` or `dataframe`)."
               },
@@ -1454,14 +1398,83 @@
             "required": [
               "type",
               "label_name"
-            ]
+            ],
+            "title": "NapariWorkflowsOutput",
+            "type": "object"
+          }
+        },
+        "additionalProperties": false,
+        "properties": {
+          "zarr_url": {
+            "title": "Zarr Url",
+            "type": "string",
+            "description": "Path or url to the individual OME-Zarr image to be processed. (standard argument for Fractal tasks, managed by Fractal server)."
+          },
+          "workflow_file": {
+            "title": "Workflow File",
+            "type": "string",
+            "description": "Absolute path to napari-workflows YAML file"
+          },
+          "input_specs": {
+            "additionalProperties": {
+              "$ref": "#/$defs/NapariWorkflowsInput"
+            },
+            "title": "Input Specs",
+            "type": "object",
+            "description": "A dictionary of `NapariWorkflowsInput` values."
+          },
+          "output_specs": {
+            "additionalProperties": {
+              "$ref": "#/$defs/NapariWorkflowsOutput"
+            },
+            "title": "Output Specs",
+            "type": "object",
+            "description": "A dictionary of `NapariWorkflowsOutput` values."
+          },
+          "input_ROI_table": {
+            "default": "FOV_ROI_table",
+            "title": "Input Roi Table",
+            "type": "string",
+            "description": "Name of the ROI table over which the task loops to apply napari workflows. Examples: `FOV_ROI_table` => loop over the field of views; `organoid_ROI_table` => loop over the organoid ROI table (generated by another task); `well_ROI_table` => process the whole well as one image."
+          },
+          "level": {
+            "default": 0,
+            "title": "Level",
+            "type": "integer",
+            "description": "Pyramid level of the image to be used as input for napari-workflows. Choose `0` to process at full resolution. Levels > 0 are currently only supported for workflows that only have intensity images as input and only produce a label images as output."
+          },
+          "relabeling": {
+            "default": true,
+            "title": "Relabeling",
+            "type": "boolean",
+            "description": "If `True`, apply relabeling so that label values are unique across all ROIs in the well."
+          },
+          "expected_dimensions": {
+            "default": 3,
+            "title": "Expected Dimensions",
+            "type": "integer",
+            "description": "Expected dimensions (either `2` or `3`). Useful when loading 2D images that are stored in a 3D array with shape `(1, size_x, size_y)` [which is the default way Fractal stores 2D images], but you want to make sure the napari workflow gets a 2D array to process. Also useful to set to `2` when loading a 2D OME-Zarr that is saved as `(size_x, size_y)`."
+          },
+          "overwrite": {
+            "default": true,
+            "title": "Overwrite",
+            "type": "boolean",
+            "description": "If `True`, overwrite the task output."
           }
-        }
+        },
+        "required": [
+          "zarr_url",
+          "workflow_file",
+          "input_specs",
+          "output_specs"
+        ],
+        "type": "object",
+        "title": "NapariWorkflowsWrapper"
       },
       "docs_info": "## napari_workflows_wrapper\nRun a napari-workflow on the ROIs of a single OME-NGFF image.\n\nThis task takes images and labels and runs a napari-workflow on them that\ncan produce a label and tables as output.\n\nExamples of allowed entries for `input_specs` and `output_specs`:\n\n```\ninput_specs = {\n    \"in_1\": {\"type\": \"image\", \"channel\": {\"wavelength_id\": \"A01_C02\"}},\n    \"in_2\": {\"type\": \"image\", \"channel\": {\"label\": \"DAPI\"}},\n    \"in_3\": {\"type\": \"label\", \"label_name\": \"label_DAPI\"},\n}\n\noutput_specs = {\n    \"out_1\": {\"type\": \"label\", \"label_name\": \"label_DAPI_new\"},\n    \"out_2\": {\"type\": \"dataframe\", \"table_name\": \"measurements\"},\n}\n```\n",
       "docs_link": "https://fractal-analytics-platform.github.io/fractal-tasks-core"
     }
   ],
   "has_args_schemas": true,
-  "args_schema_version": "pydantic_v1"
+  "args_schema_version": "pydantic_v2"
 }
diff --git a/fractal_tasks_core/channels.py b/fractal_tasks_core/channels.py
index 226323e94..965c3c24a 100644
--- a/fractal_tasks_core/channels.py
+++ b/fractal_tasks_core/channels.py
@@ -18,8 +18,10 @@
 from typing import Union
 
 import zarr
-from pydantic.v1 import BaseModel
-from pydantic.v1 import validator
+from pydantic import BaseModel
+from pydantic import field_validator
+from pydantic import model_validator
+from typing_extensions import Self
 
 from fractal_tasks_core import __OME_NGFF_VERSION__
 
@@ -43,8 +45,8 @@ class Window(BaseModel):
         end: Upper-bound rescaling value for visualization.
     """
 
-    min: Optional[int]
-    max: Optional[int]
+    min: Optional[int] = None
+    max: Optional[int] = None
     start: int
     end: int
 
@@ -69,19 +71,20 @@ class OmeroChannel(BaseModel):
     # Custom
 
     wavelength_id: str
-    index: Optional[int]
+    index: Optional[int] = None
 
     # From OME-NGFF v0.4 transitional metadata
 
-    label: Optional[str]
-    window: Optional[Window]
-    color: Optional[str]
+    label: Optional[str] = None
+    window: Optional[Window] = None
+    color: Optional[str] = None
     active: bool = True
     coefficient: int = 1
     inverted: bool = False
 
-    @validator("color", always=True)
-    def valid_hex_color(cls, v, values):
+    @field_validator("color", mode="after")
+    @classmethod
+    def valid_hex_color(cls, v: Optional[str]) -> Optional[str]:
         """
         Check that `color` is made of exactly six elements which are letters
         (a-f or A-F) or digits (0-9).
@@ -117,23 +120,24 @@ class ChannelInputModel(BaseModel):
     wavelength_id: Optional[str] = None
     label: Optional[str] = None
 
-    @validator("label", always=True)
-    def mutually_exclusive_channel_attributes(cls, v, values):
+    @model_validator(mode="after")
+    def mutually_exclusive_channel_attributes(self: Self) -> Self:
         """
         Check that either `label` or `wavelength_id` is set.
         """
-        wavelength_id = values.get("wavelength_id")
-        label = v
-        if wavelength_id and v:
+        wavelength_id = self.wavelength_id
+        label = self.label
+
+        if wavelength_id and label:
             raise ValueError(
                 "`wavelength_id` and `label` cannot be both set "
                 f"(given {wavelength_id=} and {label=})."
             )
-        if wavelength_id is None and v is None:
+        if wavelength_id is None and label is None:
             raise ValueError(
                 "`wavelength_id` and `label` cannot be both `None`"
             )
-        return v
+        return self
 
 
 class ChannelNotFoundError(ValueError):
@@ -337,7 +341,7 @@ def define_omero_channels(
             can be written to OMERO metadata.
     """
 
-    new_channels = [c.copy(deep=True) for c in channels]
+    new_channels = [c.model_copy(deep=True) for c in channels]
     default_colors = ["00FFFF", "FF00FF", "FFFF00"]
 
     for channel in new_channels:
@@ -372,7 +376,8 @@ def define_omero_channels(
         raise ValueError(f"Non-unique labels in {new_channels=}")
 
     new_channels_dictionaries = [
-        c.dict(exclude={"index"}, exclude_unset=True) for c in new_channels
+        c.model_dump(exclude={"index"}, exclude_unset=True)
+        for c in new_channels
     ]
 
     return new_channels_dictionaries
diff --git a/fractal_tasks_core/dev/check_manifest.py b/fractal_tasks_core/dev/check_manifest.py
index 19a3bbeb5..af03385c5 100644
--- a/fractal_tasks_core/dev/check_manifest.py
+++ b/fractal_tasks_core/dev/check_manifest.py
@@ -87,7 +87,7 @@ def _compare_dicts(
     # Check global properties of manifest
     if not manifest["has_args_schemas"]:
         raise ValueError(f'{manifest["has_args_schemas"]=}')
-    if manifest["args_schema_version"] != "pydantic_v1":
+    if manifest["args_schema_version"] != "pydantic_v2":
         raise ValueError(f'{manifest["args_schema_version"]=}')
 
     # Loop over tasks
diff --git a/fractal_tasks_core/dev/create_manifest.py b/fractal_tasks_core/dev/create_manifest.py
index 923386bf2..4c86f2bed 100644
--- a/fractal_tasks_core/dev/create_manifest.py
+++ b/fractal_tasks_core/dev/create_manifest.py
@@ -24,11 +24,16 @@
 from fractal_tasks_core.dev.lib_task_docs import create_docs_info
 
 
+logging.basicConfig(level=logging.INFO)
+
+
+ARGS_SCHEMA_VERSION = "pydantic_v2"
+
+
 def create_manifest(
     package: str = "fractal_tasks_core",
     manifest_version: str = "2",
     has_args_schemas: bool = True,
-    args_schema_version: str = "pydantic_v1",
     docs_link: Optional[str] = None,
     custom_pydantic_models: Optional[list[tuple[str, str, str]]] = None,
 ):
@@ -52,9 +57,6 @@ def create_manifest(
         manifest_version: Only `"2"` is supported.
         has_args_schemas:
             Whether to autogenerate JSON Schemas for task arguments.
-        args_schema_version:
-            Only `"pydantic_v1"` is currently supported in `fractal-server`
-            and `fractal-web`.
         custom_pydantic_models:
             Custom models to be included when building JSON Schemas for task
             arguments.
@@ -73,7 +75,7 @@ def create_manifest(
         has_args_schemas=has_args_schemas,
     )
     if has_args_schemas:
-        manifest["args_schema_version"] = args_schema_version
+        manifest["args_schema_version"] = ARGS_SCHEMA_VERSION
 
     # Prepare a default value of docs_link
     if package == "fractal_tasks_core" and docs_link is None:
@@ -89,7 +91,7 @@ def create_manifest(
     # to manifest["task_list"]
     for task_obj in TASK_LIST:
         # Convert Pydantic object to dictionary
-        task_dict = task_obj.dict(
+        task_dict = task_obj.model_dump(
             exclude={"meta_init", "executable_init", "meta", "executable"},
             exclude_unset=True,
         )
diff --git a/fractal_tasks_core/dev/lib_args_schemas.py b/fractal_tasks_core/dev/lib_args_schemas.py
index a28a5429e..938838e2d 100644
--- a/fractal_tasks_core/dev/lib_args_schemas.py
+++ b/fractal_tasks_core/dev/lib_args_schemas.py
@@ -21,11 +21,9 @@
 from typing import Optional
 
 from docstring_parser import parse as docparse
-from pydantic.v1.decorator import ALT_V_ARGS
-from pydantic.v1.decorator import ALT_V_KWARGS
-from pydantic.v1.decorator import V_DUPLICATE_KWARGS
-from pydantic.v1.decorator import V_POSITIONAL_ONLY_NAME
-from pydantic.v1.decorator import ValidatedFunction
+from pydantic._internal import _generate_schema
+from pydantic._internal import _typing_extra
+from pydantic._internal._config import ConfigWrapper
 
 from fractal_tasks_core.dev.lib_descriptions import (
     _get_class_attrs_descriptions,
@@ -39,6 +37,9 @@
 from fractal_tasks_core.dev.lib_descriptions import (
     _insert_function_args_descriptions,
 )
+from fractal_tasks_core.dev.lib_pydantic_generatejsonschema import (
+    CustomGenerateJsonSchema,
+)
 from fractal_tasks_core.dev.lib_signature_constraints import _extract_function
 from fractal_tasks_core.dev.lib_signature_constraints import (
     _validate_function_signature,
@@ -48,6 +49,7 @@
 
 _Schema = dict[str, Any]
 
+
 FRACTAL_TASKS_CORE_PYDANTIC_MODELS = [
     ("fractal_tasks_core", "channels.py", "OmeroChannel"),
     ("fractal_tasks_core", "channels.py", "Window"),
@@ -90,58 +92,6 @@
 ]
 
 
-def _remove_args_kwargs_properties(old_schema: _Schema) -> _Schema:
-    """
-    Remove `args` and `kwargs` schema properties.
-
-    Pydantic v1 automatically includes `args` and `kwargs` properties in
-    JSON Schemas generated via `ValidatedFunction(task_function,
-    config=None).model.schema()`, with some default (empty) values -- see see
-    https://github.com/pydantic/pydantic/blob/1.10.X-fixes/pydantic/decorator.py.
-
-    Verify that these properties match with their expected default values, and
-    then remove them from the schema.
-
-    Args:
-        old_schema: TBD
-    """
-    new_schema = old_schema.copy()
-    args_property = new_schema["properties"].pop("args")
-    kwargs_property = new_schema["properties"].pop("kwargs")
-    expected_args_property = {"title": "Args", "type": "array", "items": {}}
-    expected_kwargs_property = {"title": "Kwargs", "type": "object"}
-    if args_property != expected_args_property:
-        raise ValueError(
-            f"{args_property=}\ndiffers from\n{expected_args_property=}"
-        )
-    if kwargs_property != expected_kwargs_property:
-        raise ValueError(
-            f"{kwargs_property=}\ndiffers from\n"
-            f"{expected_kwargs_property=}"
-        )
-    logging.info("[_remove_args_kwargs_properties] END")
-    return new_schema
-
-
-def _remove_pydantic_internals(old_schema: _Schema) -> _Schema:
-    """
-    Remove schema properties that are only used internally by Pydantic V1.
-
-    Args:
-        old_schema: TBD
-    """
-    new_schema = old_schema.copy()
-    for key in (
-        V_POSITIONAL_ONLY_NAME,
-        V_DUPLICATE_KWARGS,
-        ALT_V_ARGS,
-        ALT_V_KWARGS,
-    ):
-        new_schema["properties"].pop(key, None)
-    logging.info("[_remove_pydantic_internals] END")
-    return new_schema
-
-
 def _remove_attributes_from_descriptions(old_schema: _Schema) -> _Schema:
     """
     Keeps only the description part of the docstrings: e.g from
@@ -161,16 +111,40 @@ def _remove_attributes_from_descriptions(old_schema: _Schema) -> _Schema:
         old_schema: TBD
     """
     new_schema = old_schema.copy()
-    if "definitions" in new_schema:
-        for name, definition in new_schema["definitions"].items():
-            parsed_docstring = docparse(definition["description"])
-            new_schema["definitions"][name][
-                "description"
-            ] = parsed_docstring.short_description
+    if "$defs" in new_schema:
+        for name, definition in new_schema["$defs"].items():
+            if "description" in definition.keys():
+                parsed_docstring = docparse(definition["description"])
+                new_schema["$defs"][name][
+                    "description"
+                ] = parsed_docstring.short_description
+            elif "title" in definition.keys():
+                title = definition["title"]
+                new_schema["$defs"][name][
+                    "description"
+                ] = f"Missing description for {title}."
+            else:
+                new_schema["$defs"][name][
+                    "description"
+                ] = "Missing description"
     logging.info("[_remove_attributes_from_descriptions] END")
     return new_schema
 
 
+def _create_schema_for_function(function: Callable) -> _Schema:
+    namespace = _typing_extra.add_module_globals(function, None)
+    gen_core_schema = _generate_schema.GenerateSchema(
+        ConfigWrapper(None), namespace
+    )
+    core_schema = gen_core_schema.generate_schema(function)
+    clean_core_schema = gen_core_schema.clean_schema(core_schema)
+    gen_json_schema = CustomGenerateJsonSchema()
+    json_schema = gen_json_schema.generate(
+        clean_core_schema, mode="validation"
+    )
+    return json_schema
+
+
 def create_schema_for_single_task(
     executable: str,
     package: Optional[str] = "fractal_tasks_core",
@@ -188,9 +162,10 @@ def create_schema_for_single_task(
     2. `task_function` argument is provided, `executable` is an absolute path
         to the function module, and `package` is `None. This is useful for
         testing.
-
     """
 
+    DEFINITIONS_KEY = "$defs"
+
     logging.info("[create_schema_for_single_task] START")
     if task_function is None:
         usage = "1"
@@ -243,14 +218,23 @@ def create_schema_for_single_task(
     _validate_function_signature(task_function)
 
     # Create and clean up schema
-    vf = ValidatedFunction(task_function, config=None)
-    schema = vf.model.schema()
-    schema = _remove_args_kwargs_properties(schema)
-    schema = _remove_pydantic_internals(schema)
+    schema = _create_schema_for_function(task_function)
     schema = _remove_attributes_from_descriptions(schema)
 
     # Include titles for custom-model-typed arguments
-    schema = _include_titles(schema, verbose=verbose)
+    schema = _include_titles(
+        schema, definitions_key=DEFINITIONS_KEY, verbose=verbose
+    )
+
+    # Include main title
+    if schema.get("title") is None:
+
+        def to_camel_case(snake_str):
+            return "".join(
+                x.capitalize() for x in snake_str.lower().split("_")
+            )
+
+        schema["title"] = to_camel_case(task_function.__name__)
 
     # Include descriptions of function. Note: this function works both
     # for usages 1 or 2 (see docstring).
@@ -260,8 +244,9 @@ def create_schema_for_single_task(
         function_name=function_name,
         verbose=verbose,
     )
+
     schema = _insert_function_args_descriptions(
-        schema=schema, descriptions=function_args_descriptions, verbose=verbose
+        schema=schema, descriptions=function_args_descriptions
     )
 
     # Merge lists of fractal-tasks-core and user-provided Pydantic models
@@ -293,6 +278,7 @@ def create_schema_for_single_task(
             schema=schema,
             class_name=class_name,
             descriptions=attrs_descriptions,
+            definition_key=DEFINITIONS_KEY,
         )
 
     logging.info("[create_schema_for_single_task] END")
diff --git a/fractal_tasks_core/dev/lib_descriptions.py b/fractal_tasks_core/dev/lib_descriptions.py
index e92acea3e..907b78a60 100644
--- a/fractal_tasks_core/dev/lib_descriptions.py
+++ b/fractal_tasks_core/dev/lib_descriptions.py
@@ -207,7 +207,11 @@ def _insert_function_args_descriptions(
 
 
 def _insert_class_attrs_descriptions(
-    *, schema: dict, class_name: str, descriptions: dict
+    *,
+    schema: dict,
+    class_name: str,
+    descriptions: dict,
+    definition_key: str,
 ):
     """
     Merge the descriptions obtained via `_get_attributes_models_descriptions`
@@ -217,14 +221,16 @@ def _insert_class_attrs_descriptions(
         schema: TBD
         class_name: TBD
         descriptions: TBD
+        definitions_key: Either `"definitions"` (for Pydantic V1) or
+            `"$defs"` (for Pydantic V2)
     """
     new_schema = schema.copy()
-    if "definitions" not in schema:
+    if definition_key not in schema:
         return new_schema
     else:
-        new_definitions = schema["definitions"].copy()
+        new_definitions = schema[definition_key].copy()
     # Loop over existing definitions
-    for name, definition in schema["definitions"].items():
+    for name, definition in schema[definition_key].items():
         if name == class_name:
             for prop in definition["properties"]:
                 if "description" in new_definitions[name]["properties"][prop]:
@@ -235,6 +241,6 @@ def _insert_class_attrs_descriptions(
                     new_definitions[name]["properties"][prop][
                         "description"
                     ] = descriptions[prop]
-    new_schema["definitions"] = new_definitions
+    new_schema[definition_key] = new_definitions
     logging.info("[_insert_class_attrs_descriptions] END")
     return new_schema
diff --git a/fractal_tasks_core/dev/lib_pydantic_generatejsonschema.py b/fractal_tasks_core/dev/lib_pydantic_generatejsonschema.py
new file mode 100644
index 000000000..e0a92a16d
--- /dev/null
+++ b/fractal_tasks_core/dev/lib_pydantic_generatejsonschema.py
@@ -0,0 +1,81 @@
+"""
+Custom Pydantic v2 JSON Schema generation tools.
+
+As of Pydantic V2, the JSON Schema representation of model attributes marked
+as `Optional` changed, and the new behavior consists in marking the
+corresponding properties as an `anyOf` of either a `null` or the actual type.
+This is not always the required behavior, see e.g.
+* https://github.com/pydantic/pydantic/issues/7161
+* https://github.com/pydantic/pydantic/issues/8394
+
+Here we list some alternative ways of reverting this change.
+"""
+import logging
+
+from pydantic.json_schema import GenerateJsonSchema
+from pydantic.json_schema import JsonSchemaValue
+from pydantic_core.core_schema import WithDefaultSchema
+
+logger = logging.getLogger("CustomGenerateJsonSchema")
+
+
+class CustomGenerateJsonSchema(GenerateJsonSchema):
+    def get_flattened_anyof(
+        self, schemas: list[JsonSchemaValue]
+    ) -> JsonSchemaValue:
+        null_schema = {"type": "null"}
+        if null_schema in schemas:
+            logger.warning(
+                "Drop `null_schema` before calling `get_flattened_anyof`"
+            )
+            schemas.pop(schemas.index(null_schema))
+        return super().get_flattened_anyof(schemas)
+
+    def default_schema(self, schema: WithDefaultSchema) -> JsonSchemaValue:
+        json_schema = super().default_schema(schema)
+        if "default" in json_schema.keys() and json_schema["default"] is None:
+            logger.warning(f"Pop `None` default value from {json_schema=}")
+            json_schema.pop("default")
+        return json_schema
+
+
+# class GenerateJsonSchemaA(GenerateJsonSchema):
+#     def nullable_schema(self, schema):
+#         null_schema = {"type": "null"}
+#         inner_json_schema = self.generate_inner(schema["schema"])
+#         if inner_json_schema == null_schema:
+#             return null_schema
+#         else:
+#             logging.info("A: Skip calling `get_flattened_anyof` method")
+#             return inner_json_schema
+
+
+# class GenerateJsonSchemaB(GenerateJsonSchemaA):
+#     def default_schema(self, schema: WithDefaultSchema) -> JsonSchemaValue:
+#         original_json_schema = super().default_schema(schema)
+#         new_json_schema = deepcopy(original_json_schema)
+#         default = new_json_schema.get("default", None)
+#         if default is None:
+#             logging.info("B: Pop None default")
+#             new_json_schema.pop("default")
+#         return new_json_schema
+
+
+# class GenerateJsonSchemaC(GenerateJsonSchema):
+#     def get_flattened_anyof(
+#         self, schemas: list[JsonSchemaValue]
+#     ) -> JsonSchemaValue:
+
+#         original_json_schema_value = super().get_flattened_anyof(schemas)
+#         members = original_json_schema_value.get("anyOf")
+#         logging.info("C", original_json_schema_value)
+#         if (
+#             members is not None
+#             and len(members) == 2
+#             and {"type": "null"} in members
+#         ):
+#             new_json_schema_value = {"type": [t["type"] for t in members]}
+#             logging.info("C", new_json_schema_value)
+#             return new_json_schema_value
+#         else:
+#             return original_json_schema_value
diff --git a/fractal_tasks_core/dev/lib_titles.py b/fractal_tasks_core/dev/lib_titles.py
index 07d8f3259..9d7e4e0e4 100644
--- a/fractal_tasks_core/dev/lib_titles.py
+++ b/fractal_tasks_core/dev/lib_titles.py
@@ -52,7 +52,11 @@ def _include_titles_for_properties(
     return new_properties
 
 
-def _include_titles(schema: _Schema, verbose: bool = False) -> _Schema:
+def _include_titles(
+    schema: _Schema,
+    definitions_key: str,
+    verbose: bool = False,
+) -> _Schema:
     """
     Include property titles, when missing.
 
@@ -65,6 +69,9 @@ def _include_titles(schema: _Schema, verbose: bool = False) -> _Schema:
 
     Args:
         schema: TBD
+        definitions_key: Either `"definitions"` (for Pydantic V1) or
+            `"$defs"` (for Pydantic V2)
+        verbose:
     """
     new_schema = schema.copy()
 
@@ -82,8 +89,8 @@ def _include_titles(schema: _Schema, verbose: bool = False) -> _Schema:
         logging.info("[_include_titles] Titles for properties now included.")
 
     # Update properties of definitions
-    if "definitions" in schema.keys():
-        new_definitions = schema["definitions"].copy()
+    if definitions_key in schema.keys():
+        new_definitions = schema[definitions_key].copy()
         for def_name, def_schema in new_definitions.items():
             if "properties" not in def_schema.keys():
                 if verbose:
@@ -96,7 +103,7 @@ def _include_titles(schema: _Schema, verbose: bool = False) -> _Schema:
                     def_schema["properties"], verbose=verbose
                 )
                 new_definitions[def_name]["properties"] = new_properties
-        new_schema["definitions"] = new_definitions
+        new_schema[definitions_key] = new_definitions
 
     if verbose:
         logging.info(
diff --git a/fractal_tasks_core/dev/task_models.py b/fractal_tasks_core/dev/task_models.py
index 2d2ff9835..4ce2cba04 100644
--- a/fractal_tasks_core/dev/task_models.py
+++ b/fractal_tasks_core/dev/task_models.py
@@ -18,7 +18,7 @@
 from typing import Any
 from typing import Optional
 
-from pydantic.v1 import BaseModel
+from pydantic import BaseModel
 
 
 class _BaseTask(BaseModel):
@@ -28,9 +28,9 @@ class Config:
 
     name: str
     executable: str
-    meta: Optional[dict[str, Any]]
-    input_types: Optional[dict[str, bool]]
-    output_types: Optional[dict[str, bool]]
+    meta: Optional[dict[str, Any]] = None
+    input_types: Optional[dict[str, bool]] = None
+    output_types: Optional[dict[str, bool]] = None
 
 
 class CompoundTask(_BaseTask):
@@ -41,7 +41,7 @@ class CompoundTask(_BaseTask):
     """
 
     executable_init: str
-    meta_init: Optional[dict[str, Any]]
+    meta_init: Optional[dict[str, Any]] = None
 
     @property
     def executable_non_parallel(self) -> str:
diff --git a/fractal_tasks_core/labels.py b/fractal_tasks_core/labels.py
index f15f82f91..ab9a87414 100644
--- a/fractal_tasks_core/labels.py
+++ b/fractal_tasks_core/labels.py
@@ -16,7 +16,7 @@
 from typing import Optional
 
 import zarr.hierarchy
-from pydantic.v1.error_wrappers import ValidationError
+from pydantic import ValidationError
 
 from fractal_tasks_core.ngff import NgffImageMeta
 from fractal_tasks_core.zarr_utils import OverwriteNotAllowedError
diff --git a/fractal_tasks_core/ngff/specs.py b/fractal_tasks_core/ngff/specs.py
index 62715b2fa..6930337c2 100644
--- a/fractal_tasks_core/ngff/specs.py
+++ b/fractal_tasks_core/ngff/specs.py
@@ -5,16 +5,27 @@
 import logging
 from typing import Literal
 from typing import Optional
+from typing import TypeVar
 from typing import Union
 
-from pydantic.v1 import BaseModel
-from pydantic.v1 import Field
-from pydantic.v1 import validator
+from pydantic import BaseModel
+from pydantic import Field
+from pydantic import field_validator
 
 
 logger = logging.getLogger(__name__)
 
 
+T = TypeVar("T")
+
+
+def unique_items_validator(values: list[T]) -> list[T]:
+    for ind, value in enumerate(values, start=1):
+        if value in values[ind:]:
+            raise ValueError(f"Non-unique values in {values}.")
+    return values
+
+
 class Window(BaseModel):
     """
     Model for `Channel.window`.
@@ -76,7 +87,7 @@ class ScaleCoordinateTransformation(BaseModel):
     """
 
     type: Literal["scale"]
-    scale: list[float] = Field(..., min_items=2)
+    scale: list[float] = Field(..., min_length=2)
 
 
 class TranslationCoordinateTransformation(BaseModel):
@@ -90,7 +101,7 @@ class TranslationCoordinateTransformation(BaseModel):
     """
 
     type: Literal["translation"]
-    translation: list[float] = Field(..., min_items=2)
+    translation: list[float] = Field(..., min_length=2)
 
 
 class Dataset(BaseModel):
@@ -105,7 +116,7 @@ class Dataset(BaseModel):
         Union[
             ScaleCoordinateTransformation, TranslationCoordinateTransformation
         ]
-    ] = Field(..., min_items=1)
+    ] = Field(..., min_length=1)
 
     @property
     def scale_transformation(self) -> ScaleCoordinateTransformation:
@@ -139,9 +150,9 @@ class Multiscale(BaseModel):
     """
 
     name: Optional[str] = None
-    datasets: list[Dataset] = Field(..., min_items=1)
+    datasets: list[Dataset] = Field(..., min_length=1)
     version: Optional[str] = None
-    axes: list[Axis] = Field(..., max_items=5, min_items=2, unique_items=True)
+    axes: list[Axis] = Field(..., max_length=5, min_length=2)
     coordinateTransformations: Optional[
         list[
             Union[
@@ -150,13 +161,19 @@ class Multiscale(BaseModel):
             ]
         ]
     ] = None
+    _check_unique = field_validator("axes")(unique_items_validator)
 
-    @validator("coordinateTransformations", always=True)
-    def _no_global_coordinateTransformations(cls, v):
+    @field_validator("coordinateTransformations", mode="after")
+    @classmethod
+    def _no_global_coordinateTransformations(
+        cls, v: Optional[list]
+    ) -> Optional[list]:
         """
         Fail if Multiscale has a (global) coordinateTransformations attribute.
         """
-        if v is not None:
+        if v is None:
+            return v
+        else:
             raise NotImplementedError(
                 "Global coordinateTransformations at the multiscales "
                 "level are not currently supported in the fractal-tasks-core "
@@ -174,10 +191,10 @@ class NgffImageMeta(BaseModel):
     multiscales: list[Multiscale] = Field(
         ...,
         description="The multiscale datasets for this image",
-        min_items=1,
-        unique_items=True,
+        min_length=1,
     )
     omero: Optional[Omero] = None
+    _check_unique = field_validator("multiscales")(unique_items_validator)
 
     @property
     def multiscale(self) -> Multiscale:
@@ -325,14 +342,12 @@ class Well(BaseModel):
     """
 
     images: list[ImageInWell] = Field(
-        ...,
-        description="The images included in this well",
-        min_items=1,
-        unique_items=True,
+        ..., description="The images included in this well", min_length=1
     )
     version: Optional[str] = Field(
         None, description="The version of the specification"
     )
+    _check_unique = field_validator("images")(unique_items_validator)
 
 
 class NgffWellMeta(BaseModel):
@@ -446,10 +461,10 @@ class Plate(BaseModel):
     See https://ngff.openmicroscopy.org/0.4/#plate-md.
     """
 
-    acquisitions: Optional[list[AcquisitionInPlate]]
+    acquisitions: Optional[list[AcquisitionInPlate]] = None
     columns: list[ColumnInPlate]
-    field_count: Optional[int]
-    name: Optional[str]
+    field_count: Optional[int] = None
+    name: Optional[str] = None
     rows: list[RowInPlate]
     # version will become required in 0.5
     version: Optional[str] = Field(
diff --git a/fractal_tasks_core/roi/v1_checks.py b/fractal_tasks_core/roi/v1_checks.py
index fc3aa4672..cb1193367 100644
--- a/fractal_tasks_core/roi/v1_checks.py
+++ b/fractal_tasks_core/roi/v1_checks.py
@@ -17,7 +17,7 @@
 
 import anndata as ad
 import zarr
-from pydantic.v1.error_wrappers import ValidationError
+from pydantic import ValidationError
 
 from fractal_tasks_core.tables.v1 import MaskingROITableAttrs
 
diff --git a/fractal_tasks_core/tables/v1.py b/fractal_tasks_core/tables/v1.py
index 12aa78132..96bf2f8e0 100644
--- a/fractal_tasks_core/tables/v1.py
+++ b/fractal_tasks_core/tables/v1.py
@@ -11,9 +11,9 @@
 import anndata as ad
 import zarr.hierarchy
 from anndata.experimental import write_elem
-from pydantic.v1 import BaseModel
-from pydantic.v1 import validator
-from pydantic.v1.error_wrappers import ValidationError
+from pydantic import BaseModel
+from pydantic import field_validator
+from pydantic import ValidationError
 
 from fractal_tasks_core.zarr_utils import OverwriteNotAllowedError
 
@@ -29,7 +29,8 @@ class MaskingROITableAttrs(BaseModel):
     region: _RegionType
     instance_key: str
 
-    @validator("type", always=True)
+    @field_validator("type")
+    @classmethod
     def warning_for_old_table_type(cls, v):
         if v == "ngff:region_table":
             warning_msg = (
@@ -47,7 +48,8 @@ class FeatureTableAttrs(BaseModel):
     region: _RegionType
     instance_key: str
 
-    @validator("type", always=True)
+    @field_validator("type")
+    @classmethod
     def warning_for_old_table_type(cls, v):
         if v == "ngff:region_table":
             warning_msg = (
diff --git a/fractal_tasks_core/tasks/_zarr_utils.py b/fractal_tasks_core/tasks/_zarr_utils.py
index 6d9abb850..624d9f386 100644
--- a/fractal_tasks_core/tasks/_zarr_utils.py
+++ b/fractal_tasks_core/tasks/_zarr_utils.py
@@ -65,7 +65,6 @@ def _update_well_metadata(
     """
     lock = FileLock(f"{well_url}/.zattrs.lock")
     with lock.acquire(timeout=timeout):
-
         well_meta = load_NgffWellMeta(well_url)
         existing_well_images = [image.path for image in well_meta.well.images]
         if new_image_path in existing_well_images:
@@ -94,7 +93,7 @@ def _update_well_metadata(
         )
 
         well_group = zarr.group(well_url)
-        well_group.attrs.put(well_meta.dict(exclude_none=True))
+        well_group.attrs.put(well_meta.model_dump(exclude_none=True))
 
     # One could catch the timeout with a try except Timeout. But what to do
     # with it?
diff --git a/fractal_tasks_core/tasks/apply_registration_to_image.py b/fractal_tasks_core/tasks/apply_registration_to_image.py
index d7b435ecc..2263e7f77 100644
--- a/fractal_tasks_core/tasks/apply_registration_to_image.py
+++ b/fractal_tasks_core/tasks/apply_registration_to_image.py
@@ -21,7 +21,7 @@
 import dask.array as da
 import numpy as np
 import zarr
-from pydantic.v1.decorator import validate_arguments
+from pydantic import validate_call
 
 from fractal_tasks_core.ngff import load_NgffImageMeta
 from fractal_tasks_core.ngff.zarr_utils import load_NgffWellMeta
@@ -47,7 +47,7 @@
 logger = logging.getLogger(__name__)
 
 
-@validate_arguments
+@validate_call
 def apply_registration_to_image(
     *,
     # Fractal parameters
diff --git a/fractal_tasks_core/tasks/calculate_registration_image_based.py b/fractal_tasks_core/tasks/calculate_registration_image_based.py
index d87c7d83e..0cb618c94 100755
--- a/fractal_tasks_core/tasks/calculate_registration_image_based.py
+++ b/fractal_tasks_core/tasks/calculate_registration_image_based.py
@@ -19,7 +19,7 @@
 import dask.array as da
 import numpy as np
 import zarr
-from pydantic.v1.decorator import validate_arguments
+from pydantic import validate_call
 from skimage.registration import phase_cross_correlation
 
 from fractal_tasks_core.channels import get_channel_from_image_zarr
@@ -68,7 +68,7 @@ def register(self, img_ref, img_acq_x):
             return chi2_shift_out(img_ref, img_acq_x)
 
 
-@validate_arguments
+@validate_call
 def calculate_registration_image_based(
     *,
     # Fractal arguments
diff --git a/fractal_tasks_core/tasks/cellpose_segmentation.py b/fractal_tasks_core/tasks/cellpose_segmentation.py
index d371aea24..8541d165b 100644
--- a/fractal_tasks_core/tasks/cellpose_segmentation.py
+++ b/fractal_tasks_core/tasks/cellpose_segmentation.py
@@ -26,8 +26,8 @@
 import pandas as pd
 import zarr
 from cellpose import models
-from pydantic.v1 import Field
-from pydantic.v1.decorator import validate_arguments
+from pydantic import Field
+from pydantic import validate_call
 
 import fractal_tasks_core
 from fractal_tasks_core.labels import prepare_label_group
@@ -190,7 +190,7 @@ def segment_ROI(
     return mask.astype(label_dtype)
 
 
-@validate_arguments
+@validate_call
 def cellpose_segmentation(
     *,
     # Fractal parameters
@@ -378,14 +378,14 @@ def cellpose_segmentation(
     # Workaround for #788
     if ngff_image_meta.axes_names[0] != "c":
         new_datasets = rescale_datasets(
-            datasets=[ds.dict() for ds in ngff_image_meta.datasets],
+            datasets=[ds.model_dump() for ds in ngff_image_meta.datasets],
             coarsening_xy=coarsening_xy,
             reference_level=level,
             remove_channel_axis=False,
         )
     else:
         new_datasets = rescale_datasets(
-            datasets=[ds.dict() for ds in ngff_image_meta.datasets],
+            datasets=[ds.model_dump() for ds in ngff_image_meta.datasets],
             coarsening_xy=coarsening_xy,
             reference_level=level,
             remove_channel_axis=True,
diff --git a/fractal_tasks_core/tasks/cellpose_utils.py b/fractal_tasks_core/tasks/cellpose_utils.py
index 42726496a..6ba22a29e 100644
--- a/fractal_tasks_core/tasks/cellpose_utils.py
+++ b/fractal_tasks_core/tasks/cellpose_utils.py
@@ -16,10 +16,10 @@
 from typing import Optional
 
 import numpy as np
-from pydantic.v1 import BaseModel
-from pydantic.v1 import Field
-from pydantic.v1 import root_validator
-from pydantic.v1 import validator
+from pydantic import BaseModel
+from pydantic import Field
+from pydantic import model_validator
+from typing_extensions import Self
 
 from fractal_tasks_core.channels import ChannelInputModel
 from fractal_tasks_core.channels import ChannelNotFoundError
@@ -70,14 +70,14 @@ class CellposeCustomNormalizer(BaseModel):
     # that are stored in OME-Zarr histograms and use this pydantic model that
     # those histograms actually exist
 
-    @root_validator
-    def validate_conditions(cls, values):
+    @model_validator(mode="after")
+    def validate_conditions(self: Self) -> Self:
         # Extract values
-        type = values.get("type")
-        lower_percentile = values.get("lower_percentile")
-        upper_percentile = values.get("upper_percentile")
-        lower_bound = values.get("lower_bound")
-        upper_bound = values.get("upper_bound")
+        type = self.type
+        lower_percentile = self.lower_percentile
+        upper_percentile = self.upper_percentile
+        lower_bound = self.lower_bound
+        upper_bound = self.upper_bound
 
         # Verify that custom parameters are only provided when type="custom"
         if type != "custom":
@@ -128,7 +128,7 @@ def validate_conditions(cls, values):
                 "at the same time. Hint: use only one of the two options."
             )
 
-        return values
+        return self
 
     @property
     def cellpose_normalize(self) -> bool:
@@ -256,19 +256,19 @@ class CellposeChannel2InputModel(BaseModel):
         default_factory=CellposeCustomNormalizer
     )
 
-    @validator("label", always=True)
-    def mutually_exclusive_channel_attributes(cls, v, values):
+    @model_validator(mode="after")
+    def mutually_exclusive_channel_attributes(self: Self) -> Self:
         """
         Check that only 1 of `label` or `wavelength_id` is set.
         """
-        wavelength_id = values.get("wavelength_id")
-        label = v
-        if wavelength_id and v:
+        wavelength_id = self.wavelength_id
+        label = self.label
+        if (wavelength_id is not None) and (label is not None):
             raise ValueError(
                 "`wavelength_id` and `label` cannot be both set "
                 f"(given {wavelength_id=} and {label=})."
             )
-        return v
+        return self
 
     def is_set(self):
         if self.wavelength_id or self.label:
diff --git a/fractal_tasks_core/tasks/cellvoyager_to_ome_zarr_compute.py b/fractal_tasks_core/tasks/cellvoyager_to_ome_zarr_compute.py
index fc1ae6380..b575b931e 100644
--- a/fractal_tasks_core/tasks/cellvoyager_to_ome_zarr_compute.py
+++ b/fractal_tasks_core/tasks/cellvoyager_to_ome_zarr_compute.py
@@ -18,7 +18,7 @@
 import zarr
 from anndata import read_zarr
 from dask.array.image import imread
-from pydantic.v1.decorator import validate_arguments
+from pydantic import validate_call
 
 from fractal_tasks_core.cellvoyager.filenames import (
     glob_with_multiple_patterns,
@@ -53,7 +53,7 @@ def sort_fun(filename: str) -> list[int]:
     return [site, z_index]
 
 
-@validate_arguments
+@validate_call
 def cellvoyager_to_ome_zarr_compute(
     *,
     # Fractal parameters
diff --git a/fractal_tasks_core/tasks/cellvoyager_to_ome_zarr_init.py b/fractal_tasks_core/tasks/cellvoyager_to_ome_zarr_init.py
index 831dde3c2..127fc1f72 100644
--- a/fractal_tasks_core/tasks/cellvoyager_to_ome_zarr_init.py
+++ b/fractal_tasks_core/tasks/cellvoyager_to_ome_zarr_init.py
@@ -17,7 +17,7 @@
 from typing import Optional
 
 import pandas as pd
-from pydantic.v1.decorator import validate_arguments
+from pydantic import validate_call
 
 import fractal_tasks_core
 from fractal_tasks_core.cellvoyager.filenames import (
@@ -49,7 +49,7 @@
 logger = logging.getLogger(__name__)
 
 
-@validate_arguments
+@validate_call
 def cellvoyager_to_ome_zarr_init(
     *,
     # Fractal parameters
@@ -371,7 +371,7 @@ def cellvoyager_to_ome_zarr_init(
                         well_ID=get_filename_well_id(row, column),
                         image_extension=image_extension,
                         image_glob_patterns=image_glob_patterns,
-                    ).dict(),
+                    ).model_dump(),
                 }
             )
             group_well = group_plate.create_group(f"{row}/{column}/")
diff --git a/fractal_tasks_core/tasks/cellvoyager_to_ome_zarr_init_multiplex.py b/fractal_tasks_core/tasks/cellvoyager_to_ome_zarr_init_multiplex.py
index 723e4f9c9..b1e521952 100644
--- a/fractal_tasks_core/tasks/cellvoyager_to_ome_zarr_init_multiplex.py
+++ b/fractal_tasks_core/tasks/cellvoyager_to_ome_zarr_init_multiplex.py
@@ -18,7 +18,7 @@
 
 import pandas as pd
 import zarr
-from pydantic.v1.decorator import validate_arguments
+from pydantic import validate_call
 from zarr.errors import ContainsGroupError
 
 import fractal_tasks_core
@@ -52,7 +52,7 @@
 logger = logging.getLogger(__name__)
 
 
-@validate_arguments
+@validate_call
 def cellvoyager_to_ome_zarr_init_multiplex(
     *,
     # Fractal parameters
@@ -115,7 +115,6 @@ def cellvoyager_to_ome_zarr_init_multiplex(
     """
 
     if metadata_table_files:
-
         # Checks on the dict:
         # 1. Acquisitions in acquisitions dict and metadata_table_files match
         # 2. Files end with ".csv"
@@ -268,7 +267,6 @@ def cellvoyager_to_ome_zarr_init_multiplex(
     logging.info(f"{acquisitions_sorted=}")
 
     for acquisition in acquisitions_sorted:
-
         # Define plate zarr
         image_folder = dict_acquisitions[acquisition]["image_folder"]
         logger.info(f"Looking at {image_folder=}")
@@ -382,7 +380,7 @@ def cellvoyager_to_ome_zarr_init_multiplex(
                         image_extension=image_extension,
                         image_glob_patterns=image_glob_patterns,
                         acquisition=acquisition,
-                    ).dict(),
+                    ).model_dump(),
                 }
             )
             try:
diff --git a/fractal_tasks_core/tasks/copy_ome_zarr_hcs_plate.py b/fractal_tasks_core/tasks/copy_ome_zarr_hcs_plate.py
index 9196fda01..9c491e0ad 100644
--- a/fractal_tasks_core/tasks/copy_ome_zarr_hcs_plate.py
+++ b/fractal_tasks_core/tasks/copy_ome_zarr_hcs_plate.py
@@ -15,7 +15,7 @@
 import logging
 from typing import Any
 
-from pydantic.v1.decorator import validate_arguments
+from pydantic import validate_call
 
 import fractal_tasks_core
 from fractal_tasks_core.ngff.specs import NgffPlateMeta
@@ -181,12 +181,12 @@ def _generate_plate_well_metadata(
         # Validate with NgffPlateMeta model
         plate_metadata_dicts[old_plate_url] = NgffPlateMeta(
             **plate_metadata_dicts[old_plate_url]
-        ).dict(exclude_none=True)
+        ).model_dump(exclude_none=True)
 
     return plate_metadata_dicts, new_well_image_attrs, well_image_attrs
 
 
-@validate_arguments
+@validate_call
 def copy_ome_zarr_hcs_plate(
     *,
     # Fractal parameters
@@ -277,7 +277,7 @@ def copy_ome_zarr_hcs_plate(
             well_attrs = dict(
                 well=dict(
                     images=[
-                        img.dict(exclude_none=True)
+                        img.model_dump(exclude_none=True)
                         for img in new_well_image_attrs[old_plate_url][
                             well_sub_url
                         ]
diff --git a/fractal_tasks_core/tasks/find_registration_consensus.py b/fractal_tasks_core/tasks/find_registration_consensus.py
index b241c8e3c..f0aadb010 100644
--- a/fractal_tasks_core/tasks/find_registration_consensus.py
+++ b/fractal_tasks_core/tasks/find_registration_consensus.py
@@ -17,7 +17,7 @@
 
 import anndata as ad
 import zarr
-from pydantic.v1.decorator import validate_arguments
+from pydantic import validate_call
 
 from fractal_tasks_core.roi import (
     are_ROI_table_columns_valid,
@@ -38,7 +38,7 @@
 logger = logging.getLogger(__name__)
 
 
-@validate_arguments
+@validate_call
 def find_registration_consensus(
     *,
     # Fractal parameters
diff --git a/fractal_tasks_core/tasks/illumination_correction.py b/fractal_tasks_core/tasks/illumination_correction.py
index 646b47570..edb045b7b 100644
--- a/fractal_tasks_core/tasks/illumination_correction.py
+++ b/fractal_tasks_core/tasks/illumination_correction.py
@@ -22,7 +22,7 @@
 import dask.array as da
 import numpy as np
 import zarr
-from pydantic.v1.decorator import validate_arguments
+from pydantic import validate_call
 from skimage.io import imread
 
 from fractal_tasks_core.channels import get_omero_channel_list
@@ -92,7 +92,7 @@ def correct(
     return new_img_stack.astype(dtype)
 
 
-@validate_arguments
+@validate_call
 def illumination_correction(
     *,
     # Fractal parameters
diff --git a/fractal_tasks_core/tasks/image_based_registration_hcs_init.py b/fractal_tasks_core/tasks/image_based_registration_hcs_init.py
index 47989280d..abb43c513 100644
--- a/fractal_tasks_core/tasks/image_based_registration_hcs_init.py
+++ b/fractal_tasks_core/tasks/image_based_registration_hcs_init.py
@@ -15,7 +15,7 @@
 import logging
 from typing import Any
 
-from pydantic.v1.decorator import validate_arguments
+from pydantic import validate_call
 
 from fractal_tasks_core.tasks._registration_utils import (
     create_well_acquisition_dict,
@@ -24,7 +24,7 @@
 logger = logging.getLogger(__name__)
 
 
-@validate_arguments
+@validate_call
 def image_based_registration_hcs_init(
     *,
     # Fractal parameters
diff --git a/fractal_tasks_core/tasks/import_ome_zarr.py b/fractal_tasks_core/tasks/import_ome_zarr.py
index c720654e3..f35bdd48f 100644
--- a/fractal_tasks_core/tasks/import_ome_zarr.py
+++ b/fractal_tasks_core/tasks/import_ome_zarr.py
@@ -17,7 +17,7 @@
 
 import dask.array as da
 import zarr
-from pydantic.v1.decorator import validate_arguments
+from pydantic import validate_call
 
 from fractal_tasks_core.channels import update_omero_channels
 from fractal_tasks_core.ngff import detect_ome_ngff_type
@@ -159,7 +159,7 @@ def _process_single_image(
     return types
 
 
-@validate_arguments
+@validate_call
 def import_ome_zarr(
     *,
     # Fractal parameters
@@ -306,7 +306,6 @@ def import_ome_zarr(
 
 
 if __name__ == "__main__":
-
     from fractal_tasks_core.tasks._utils import run_fractal_task
 
     run_fractal_task(
diff --git a/fractal_tasks_core/tasks/init_group_by_well_for_multiplexing.py b/fractal_tasks_core/tasks/init_group_by_well_for_multiplexing.py
index 6f6bff63a..fb13faebe 100644
--- a/fractal_tasks_core/tasks/init_group_by_well_for_multiplexing.py
+++ b/fractal_tasks_core/tasks/init_group_by_well_for_multiplexing.py
@@ -14,7 +14,7 @@
 """
 import logging
 
-from pydantic.v1.decorator import validate_arguments
+from pydantic import validate_call
 
 from fractal_tasks_core.tasks._registration_utils import (
     create_well_acquisition_dict,
@@ -23,7 +23,7 @@
 logger = logging.getLogger(__name__)
 
 
-@validate_arguments
+@validate_call
 def init_group_by_well_for_multiplexing(
     *,
     # Fractal parameters
diff --git a/fractal_tasks_core/tasks/io_models.py b/fractal_tasks_core/tasks/io_models.py
index 93e4425c4..c33dc0869 100644
--- a/fractal_tasks_core/tasks/io_models.py
+++ b/fractal_tasks_core/tasks/io_models.py
@@ -1,9 +1,10 @@
 from typing import Literal
 from typing import Optional
 
-from pydantic.v1 import BaseModel
-from pydantic.v1 import Field
-from pydantic.v1 import validator
+from pydantic import BaseModel
+from pydantic import Field
+from pydantic import model_validator
+from typing_extensions import Self
 
 from fractal_tasks_core.channels import ChannelInputModel
 from fractal_tasks_core.channels import OmeroChannel
@@ -58,8 +59,8 @@ class InitArgsCellVoyager(BaseModel):
     plate_prefix: str
     well_ID: str
     image_extension: str
-    image_glob_patterns: Optional[list[str]]
-    acquisition: Optional[int]
+    image_glob_patterns: Optional[list[str]] = None
+    acquisition: Optional[int] = None
 
 
 class InitArgsIllumination(BaseModel):
@@ -119,17 +120,20 @@ class NapariWorkflowsOutput(BaseModel):
     label_name: str
     table_name: Optional[str] = None
 
-    @validator("table_name", always=True)
-    def table_name_only_for_dataframe_type(cls, v, values):
+    @model_validator(mode="after")
+    def table_name_only_for_dataframe_type(self: Self) -> Self:
         """
         Check that table_name is set only for dataframe outputs.
         """
-        _type = values.get("type")
-        if (_type == "dataframe" and (not v)) or (_type != "dataframe" and v):
+        _type = self.type
+        _table_name = self.table_name
+        if (_type == "dataframe" and (not _table_name)) or (
+            _type != "dataframe" and _table_name
+        ):
             raise ValueError(
-                f"Output item has type={_type} but table_name={v}."
+                f"Output item has type={_type} but table_name={_table_name}."
             )
-        return v
+        return self
 
 
 class NapariWorkflowsInput(BaseModel):
@@ -143,27 +147,31 @@ class NapariWorkflowsInput(BaseModel):
     """
 
     type: Literal["image", "label"]
-    label_name: Optional[str]
-    channel: Optional[ChannelInputModel]
+    label_name: Optional[str] = None
+    channel: Optional[ChannelInputModel] = None
 
-    @validator("label_name", always=True)
-    def label_name_is_present(cls, v, values):
+    @model_validator(mode="after")
+    def label_name_is_present(self: Self) -> Self:
         """
         Check that label inputs have `label_name` set.
         """
-        _type = values.get("type")
-        if _type == "label" and not v:
+        label_name = self.label_name
+        _type = self.type
+        if _type == "label" and label_name is None:
             raise ValueError(
-                f"Input item has type={_type} but label_name={v}."
+                f"Input item has type={_type} but label_name={label_name}."
             )
-        return v
+        return self
 
-    @validator("channel", always=True)
-    def channel_is_present(cls, v, values):
+    @model_validator(mode="after")
+    def channel_is_present(self: Self) -> Self:
         """
         Check that image inputs have `channel` set.
         """
-        _type = values.get("type")
-        if _type == "image" and not v:
-            raise ValueError(f"Input item has type={_type} but channel={v}.")
-        return v
+        _type = self.type
+        channel = self.channel
+        if _type == "image" and channel is None:
+            raise ValueError(
+                f"Input item has type={_type} but channel={channel}."
+            )
+        return self
diff --git a/fractal_tasks_core/tasks/maximum_intensity_projection.py b/fractal_tasks_core/tasks/maximum_intensity_projection.py
index bec0f72cf..0cdc5ce02 100644
--- a/fractal_tasks_core/tasks/maximum_intensity_projection.py
+++ b/fractal_tasks_core/tasks/maximum_intensity_projection.py
@@ -18,7 +18,7 @@
 import anndata as ad
 import dask.array as da
 import zarr
-from pydantic.v1.decorator import validate_arguments
+from pydantic import validate_call
 from zarr.errors import ContainsArrayError
 
 from fractal_tasks_core.ngff import load_NgffImageMeta
@@ -35,7 +35,7 @@
 logger = logging.getLogger(__name__)
 
 
-@validate_arguments
+@validate_call
 def maximum_intensity_projection(
     *,
     # Fractal parameters
@@ -63,7 +63,7 @@ def maximum_intensity_projection(
     ngff_image = load_NgffImageMeta(init_args.origin_url)
     # Currently not using the validation models due to wavelength_id issue
     # See #681 for discussion
-    # new_attrs = ngff_image.dict(exclude_none=True)
+    # new_attrs = ngff_image.model_dump(exclude_none=True)
     # Current way to get the necessary metadata for MIP
     group = zarr.open_group(init_args.origin_url, mode="r")
     new_attrs = group.attrs.asdict()
@@ -183,7 +183,6 @@ def maximum_intensity_projection(
 
 
 if __name__ == "__main__":
-
     from fractal_tasks_core.tasks._utils import run_fractal_task
 
     run_fractal_task(
diff --git a/fractal_tasks_core/tasks/napari_workflows_wrapper.py b/fractal_tasks_core/tasks/napari_workflows_wrapper.py
index c37b1a7a4..b381a5960 100644
--- a/fractal_tasks_core/tasks/napari_workflows_wrapper.py
+++ b/fractal_tasks_core/tasks/napari_workflows_wrapper.py
@@ -22,7 +22,7 @@
 import pandas as pd
 import zarr
 from napari_workflows._io_yaml_v1 import load_workflow
-from pydantic.v1.decorator import validate_arguments
+from pydantic import validate_call
 
 import fractal_tasks_core
 from fractal_tasks_core.channels import get_channel_from_image_zarr
@@ -59,7 +59,7 @@ class OutOfTaskScopeError(NotImplementedError):
     pass
 
 
-@validate_arguments
+@validate_call
 def napari_workflows_wrapper(
     *,
     # Fractal parameters
@@ -394,7 +394,6 @@ def napari_workflows_wrapper(
         # Loop over label outputs and (1) set zattrs, (2) create zarr group
         output_label_zarr_groups: dict[str, Any] = {}
         for name, out_params in label_outputs:
-
             # (1a) Rescale OME-NGFF datasets (relevant for level>0)
             if not ngff_image_meta.multiscale.axes[0].name == "c":
                 raise ValueError(
@@ -404,7 +403,8 @@ def napari_workflows_wrapper(
                 )
             new_datasets = rescale_datasets(
                 datasets=[
-                    ds.dict() for ds in ngff_image_meta.multiscale.datasets
+                    ds.model_dump()
+                    for ds in ngff_image_meta.multiscale.datasets
                 ],
                 coarsening_xy=coarsening_xy,
                 reference_level=level,
@@ -423,7 +423,7 @@ def napari_workflows_wrapper(
                         "name": label_name,
                         "version": __OME_NGFF_VERSION__,
                         "axes": [
-                            ax.dict()
+                            ax.model_dump()
                             for ax in ngff_image_meta.multiscale.axes
                             if ax.type != "channel"
                         ],
diff --git a/poetry.lock b/poetry.lock
index c6612dca2..7435e12af 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -2624,7 +2624,7 @@ testing = ["coverage", "pyyaml"]
 name = "markdown-it-py"
 version = "3.0.0"
 description = "Python port of markdown-it. Markdown parsing, done right!"
-optional = false
+optional = true
 python-versions = ">=3.8"
 files = [
     {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
@@ -2803,7 +2803,7 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
 name = "mdurl"
 version = "0.1.2"
 description = "Markdown URL utilities"
-optional = false
+optional = true
 python-versions = ">=3.7"
 files = [
     {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
@@ -3108,8 +3108,8 @@ napari-plugin-engine = ">=0.1.9"
 napari-svg = ">=0.1.8"
 npe2 = ">=0.7.6"
 numpy = [
-    {version = ">=1.22.2,<2", markers = "python_version < \"3.10\""},
     {version = ">=1.22.2", markers = "python_version >= \"3.10\""},
+    {version = ">=1.22.2,<2", markers = "python_version < \"3.10\""},
 ]
 numpydoc = ">=0.9.2"
 pandas = ">=1.3.0"
@@ -3855,12 +3855,12 @@ files = [
 
 [package.dependencies]
 numpy = [
-    {version = ">=1.19.3", markers = "platform_system == \"Linux\" and platform_machine == \"aarch64\" and python_version >= \"3.8\" and python_version < \"3.10\" or python_version > \"3.9\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_system != \"Darwin\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_machine != \"arm64\" and python_version < \"3.10\""},
     {version = ">=1.26.0", markers = "python_version >= \"3.12\""},
-    {version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""},
-    {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""},
     {version = ">=1.21.0", markers = "python_version == \"3.9\" and platform_system == \"Darwin\" and platform_machine == \"arm64\""},
+    {version = ">=1.19.3", markers = "platform_system == \"Linux\" and platform_machine == \"aarch64\" and python_version >= \"3.8\" and python_version < \"3.10\" or python_version > \"3.9\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_system != \"Darwin\" and python_version < \"3.10\" or python_version >= \"3.9\" and platform_machine != \"arm64\" and python_version < \"3.10\""},
+    {version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""},
     {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""},
+    {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""},
 ]
 
 [[package]]
@@ -3923,8 +3923,8 @@ files = [
 
 [package.dependencies]
 numpy = [
-    {version = ">=1.20.3", markers = "python_version < \"3.10\""},
     {version = ">=1.23.2", markers = "python_version >= \"3.11\""},
+    {version = ">=1.20.3", markers = "python_version < \"3.10\""},
     {version = ">=1.21.0", markers = "python_version >= \"3.10\" and python_version < \"3.11\""},
 ]
 python-dateutil = ">=2.8.1"
@@ -4388,8 +4388,8 @@ files = [
 annotated-types = ">=0.4.0"
 pydantic-core = "2.20.1"
 typing-extensions = [
-    {version = ">=4.6.1", markers = "python_version < \"3.13\""},
     {version = ">=4.12.2", markers = "python_version >= \"3.13\""},
+    {version = ">=4.6.1", markers = "python_version < \"3.13\""},
 ]
 
 [package.extras]
@@ -4641,21 +4641,6 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
 [package.extras]
 testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
 
-[[package]]
-name = "pytest-pretty"
-version = "1.2.0"
-description = "pytest plugin for printing summary data as I want it"
-optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "pytest_pretty-1.2.0-py3-none-any.whl", hash = "sha256:6f79122bf53864ae2951b6c9e94d7a06a87ef753476acd4588aeac018f062036"},
-    {file = "pytest_pretty-1.2.0.tar.gz", hash = "sha256:105a355f128e392860ad2c478ae173ff96d2f03044692f9818ff3d49205d3a60"},
-]
-
-[package.dependencies]
-pytest = ">=7"
-rich = ">=12"
-
 [[package]]
 name = "python-dateutil"
 version = "2.9.0.post0"
@@ -5099,7 +5084,7 @@ files = [
 name = "rich"
 version = "13.7.1"
 description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
-optional = false
+optional = true
 python-versions = ">=3.7.0"
 files = [
     {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"},
@@ -6297,4 +6282,4 @@ fractal-tasks = ["Pillow", "cellpose", "image_registration", "imageio-ffmpeg", "
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.9"
-content-hash = "b42d97668d69651036d3be301e072e61f4546f2cfbb6f332a3946e626494ca38"
+content-hash = "824fe40b5aa9a15003ffcda259d6bdd9cd45e193604497127264d59eb0e84106"
diff --git a/pyproject.toml b/pyproject.toml
index 93272ecd3..b3d29f882 100755
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -31,7 +31,7 @@ numpy = "<2"
 pandas = ">=1.2.0,<2"
 defusedxml = "^0.7.1"
 lxml = "^4.9.1"
-pydantic = ">=1.10.16"
+pydantic = ">2"
 docstring-parser = "^0.15"
 anndata = ">=0.8.0,<0.11.0"
 filelock = "3.13.*"
@@ -60,7 +60,6 @@ pre-commit = "^2.19.0"
 pytest = "^7.1.2"
 bumpver = "^2022.1118"
 coverage = {extras = ["toml"], version = "^6.5.0"}
-pytest-pretty = "^1.1.0"
 jsonschema = "^4.16.0"
 mypy = "^1.3.0"
 requests = ">=2.28.0"


Test patch:
diff --git a/tests/data/fake_package_for_schema_experiments/my_package/__FRACTAL_MANIFEST__.json b/tests/data/fake_package_for_schema_experiments/my_package/__FRACTAL_MANIFEST__.json
index 77e58fc84..0f5a44ec2 100644
--- a/tests/data/fake_package_for_schema_experiments/my_package/__FRACTAL_MANIFEST__.json
+++ b/tests/data/fake_package_for_schema_experiments/my_package/__FRACTAL_MANIFEST__.json
@@ -4,70 +4,29 @@
       "name": "Task Name",
       "executable": "tasks/my_task.py",
       "args_schema": {
-        "title": "MyTask",
-        "type": "object",
-        "properties": {
-          "x": {
-            "title": "X",
-            "type": "integer",
-            "description": "Missing description"
-          },
-          "y": {
-            "$ref": "#/definitions/OmeroChannel",
-            "description": "Missing description"
-          },
-          "z": {
-            "$ref": "#/definitions/CustomModel",
-            "description": "Missing description"
-          }
-        },
-        "required": [
-          "x",
-          "y",
-          "z"
-        ],
-        "additionalProperties": false,
-        "definitions": {
-          "Window": {
-            "title": "Window",
-            "description": "Custom class for Omero-channel window, based on OME-NGFF v0.4.",
-            "type": "object",
+        "$defs": {
+          "CustomModel": {
+            "description": "Short description",
             "properties": {
-              "min": {
-                "title": "Min",
-                "type": "integer",
-                "description": "Do not change. It will be set to ``0`` by default."
-              },
-              "max": {
-                "title": "Max",
-                "type": "integer",
-                "description": "Do not change. It will be set according to bit-depth of the images by default (e.g. 65535 for 16 bit images)."
-              },
-              "start": {
-                "title": "Start",
-                "type": "integer",
-                "description": "Lower-bound rescaling value for visualization."
-              },
-              "end": {
-                "title": "End",
+              "x": {
+                "title": "X",
                 "type": "integer",
-                "description": "Upper-bound rescaling value for visualization."
+                "description": "Description of `x`"
               }
             },
             "required": [
-              "start",
-              "end"
-            ]
+              "x"
+            ],
+            "title": "CustomModel",
+            "type": "object"
           },
           "OmeroChannel": {
-            "title": "OmeroChannel",
             "description": "Custom class for Omero channels, based on OME-NGFF v0.4.",
-            "type": "object",
             "properties": {
               "wavelength_id": {
                 "title": "Wavelength Id",
                 "type": "string",
-                "description": "Unique ID for the channel wavelength, e.g. ``A01_C01``."
+                "description": "Unique ID for the channel wavelength, e.g. `A01_C01`."
               },
               "index": {
                 "title": "Index",
@@ -77,58 +36,107 @@
               "label": {
                 "title": "Label",
                 "type": "string",
-                "description": "Name of the channel"
+                "description": "Name of the channel."
               },
               "window": {
-                "$ref": "#/definitions/Window",
-                "description": "Optional ``Window`` object to set default display settings for napari."
+                "allOf": [
+                  {
+                    "$ref": "#/$defs/Window"
+                  }
+                ],
+                "title": "Window",
+                "description": "Optional `Window` object to set default display settings for napari."
               },
               "color": {
                 "title": "Color",
                 "type": "string",
-                "description": "Optional hex colormap to display the channel in napari (e.g. ``00FFFF``)."
+                "description": "Optional hex colormap to display the channel in napari (it must be of length 6, e.g. `00FFFF`)."
               },
               "active": {
-                "title": "Active",
                 "default": true,
+                "title": "Active",
                 "type": "boolean",
                 "description": "Should this channel be shown in the viewer?"
               },
               "coefficient": {
-                "title": "Coefficient",
                 "default": 1,
+                "title": "Coefficient",
                 "type": "integer",
-                "description": "Do not change. Omero-channel attribute. "
+                "description": "Do not change. Omero-channel attribute."
               },
               "inverted": {
-                "title": "Inverted",
                 "default": false,
+                "title": "Inverted",
                 "type": "boolean",
                 "description": "Do not change. Omero-channel attribute."
               }
             },
             "required": [
               "wavelength_id"
-            ]
+            ],
+            "title": "OmeroChannel",
+            "type": "object"
           },
-          "CustomModel": {
-            "title": "CustomModel",
-            "type": "object",
+          "Window": {
+            "description": "Custom class for Omero-channel window, based on OME-NGFF v0.4.",
             "properties": {
-              "x": {
-                "title": "X",
+              "min": {
+                "title": "Min",
+                "type": "integer",
+                "description": "Do not change. It will be set to `0` by default."
+              },
+              "max": {
+                "title": "Max",
+                "type": "integer",
+                "description": "Do not change. It will be set according to bit-depth of the images by default (e.g. 65535 for 16 bit images)."
+              },
+              "start": {
+                "title": "Start",
+                "type": "integer",
+                "description": "Lower-bound rescaling value for visualization."
+              },
+              "end": {
+                "title": "End",
                 "type": "integer",
-                "description": "Missing description"
+                "description": "Upper-bound rescaling value for visualization."
               }
             },
             "required": [
-              "x"
-            ]
+              "start",
+              "end"
+            ],
+            "title": "Window",
+            "type": "object"
+          }
+        },
+        "additionalProperties": false,
+        "properties": {
+          "x": {
+            "title": "X",
+            "type": "integer",
+            "description": "Missing description"
+          },
+          "y": {
+            "$ref": "#/$defs/OmeroChannel",
+            "title": "Y",
+            "description": "Missing description"
+          },
+          "z": {
+            "$ref": "#/$defs/CustomModel",
+            "title": "Z",
+            "description": "Missing description"
           }
-        }
+        },
+        "required": [
+          "x",
+          "y",
+          "z"
+        ],
+        "type": "object",
+        "title": "MyTask"
       }
     }
   ],
   "has_args_schemas": true,
-  "args_schema_version": "pydantic_v1"
+  "args_schema_version": "pydantic_v2"
 }
diff --git a/tests/data/fake_package_for_schema_experiments/my_package/lib_custom_models.py b/tests/data/fake_package_for_schema_experiments/my_package/lib_custom_models.py
index 0fba2d6df..73386e699 100644
--- a/tests/data/fake_package_for_schema_experiments/my_package/lib_custom_models.py
+++ b/tests/data/fake_package_for_schema_experiments/my_package/lib_custom_models.py
@@ -1,5 +1,12 @@
-from pydantic.v1 import BaseModel
+from pydantic import BaseModel
 
 
 class CustomModel(BaseModel):
+    """
+    Short description
+
+    Attributes:
+        x: Description of `x`
+    """
+
     x: int
diff --git a/tests/data/fake_package_for_schema_experiments/new_args_schema.py b/tests/data/fake_package_for_schema_experiments/new_args_schema.py
index 449b092d5..d227f1c82 100644
--- a/tests/data/fake_package_for_schema_experiments/new_args_schema.py
+++ b/tests/data/fake_package_for_schema_experiments/new_args_schema.py
@@ -35,7 +35,7 @@
 
     # Set global properties of manifest
     manifest["has_args_schemas"] = True
-    manifest["args_schema_version"] = "pydantic_v1"
+    manifest["args_schema_version"] = "pydantic_v2"
 
     # Loop over tasks and set args schemas
     task_list = manifest["task_list"]
diff --git a/tests/data/generate_zarr_ones.py b/tests/data/generate_zarr_ones.py
index 26e71ac00..0aeea0768 100644
--- a/tests/data/generate_zarr_ones.py
+++ b/tests/data/generate_zarr_ones.py
@@ -6,9 +6,9 @@
 import numpy as np
 import pandas as pd
 import zarr
-from anndata._io.specs import write_elem
 
 from fractal_tasks_core.roi import prepare_FOV_ROI_table
+from fractal_tasks_core.tables import write_table
 
 
 num_C = 2
@@ -74,7 +74,7 @@
             "axes": axes,
             "datasets": [
                 {
-                    "path": level,
+                    "path": str(level),
                     cT: [
                         {
                             "type": "scale",
@@ -146,5 +146,11 @@
 FOV_ROI_table = prepare_FOV_ROI_table(df)
 print(FOV_ROI_table.to_df())
 
-group_tables = zarr.group(f"{zarrurl}{component}/tables")
-write_elem(group_tables, "FOV_ROI_table", FOV_ROI_table)
+image_group = zarr.group(f"{zarrurl}{component}")
+write_table(
+    image_group,
+    "FOV_ROI_table",
+    FOV_ROI_table,
+    overwrite=True,
+    table_attrs=dict(fractal_table_version="1", type="roi_table"),
+)
diff --git a/tests/data/ngff_examples/dataset.json b/tests/data/ngff_examples/dataset.json
index d81fe3fba..0ca64c571 100644
--- a/tests/data/ngff_examples/dataset.json
+++ b/tests/data/ngff_examples/dataset.json
@@ -1,5 +1,5 @@
 {
-    "path": 0,
+    "path": "0",
     "coordinateTransformations": [
         {
             "type": "scale",
diff --git a/tests/data/ngff_examples/dataset_error_1.json b/tests/data/ngff_examples/dataset_error_1.json
index b81702c3d..33d239e56 100644
--- a/tests/data/ngff_examples/dataset_error_1.json
+++ b/tests/data/ngff_examples/dataset_error_1.json
@@ -1,5 +1,5 @@
 {
-    "path": 0,
+    "path": "0",
     "coordinateTransformations": [
         {
             "type": "translation",
diff --git a/tests/data/ngff_examples/dataset_error_2.json b/tests/data/ngff_examples/dataset_error_2.json
index 16c717084..64ade7a68 100644
--- a/tests/data/ngff_examples/dataset_error_2.json
+++ b/tests/data/ngff_examples/dataset_error_2.json
@@ -1,5 +1,5 @@
 {
-    "path": 0,
+    "path": "0",
     "coordinateTransformations": [
         {
             "type": "scale",
diff --git a/tests/data/ngff_examples/image.json b/tests/data/ngff_examples/image.json
index ed7b031cc..db9eec1f8 100644
--- a/tests/data/ngff_examples/image.json
+++ b/tests/data/ngff_examples/image.json
@@ -24,7 +24,7 @@
             ],
             "datasets": [
                 {
-                    "path": 0,
+                    "path": "0",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -38,7 +38,7 @@
                     ]
                 },
                 {
-                    "path": 1,
+                    "path": "1",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -52,7 +52,7 @@
                     ]
                 },
                 {
-                    "path": 2,
+                    "path": "2",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -66,7 +66,7 @@
                     ]
                 },
                 {
-                    "path": 3,
+                    "path": "3",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -80,7 +80,7 @@
                     ]
                 },
                 {
-                    "path": 4,
+                    "path": "4",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
diff --git a/tests/data/ngff_examples/image_CYX.json b/tests/data/ngff_examples/image_CYX.json
index 32e4f352a..2bbcfb440 100644
--- a/tests/data/ngff_examples/image_CYX.json
+++ b/tests/data/ngff_examples/image_CYX.json
@@ -19,7 +19,7 @@
             ],
             "datasets": [
                 {
-                    "path": 0,
+                    "path": "0",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -32,7 +32,7 @@
                     ]
                 },
                 {
-                    "path": 1,
+                    "path": "1",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -45,7 +45,7 @@
                     ]
                 },
                 {
-                    "path": 2,
+                    "path": "2",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -58,7 +58,7 @@
                     ]
                 },
                 {
-                    "path": 3,
+                    "path": "3",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -71,7 +71,7 @@
                     ]
                 },
                 {
-                    "path": 4,
+                    "path": "4",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
diff --git a/tests/data/ngff_examples/image_ZYX.json b/tests/data/ngff_examples/image_ZYX.json
index b9bfeb4b8..2eca8b008 100644
--- a/tests/data/ngff_examples/image_ZYX.json
+++ b/tests/data/ngff_examples/image_ZYX.json
@@ -20,7 +20,7 @@
             ],
             "datasets": [
                 {
-                    "path": 0,
+                    "path": "0",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -33,7 +33,7 @@
                     ]
                 },
                 {
-                    "path": 1,
+                    "path": "1",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -46,7 +46,7 @@
                     ]
                 },
                 {
-                    "path": 2,
+                    "path": "2",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -59,7 +59,7 @@
                     ]
                 },
                 {
-                    "path": 3,
+                    "path": "3",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -72,7 +72,7 @@
                     ]
                 },
                 {
-                    "path": 4,
+                    "path": "4",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
diff --git a/tests/data/ngff_examples/image_error.json b/tests/data/ngff_examples/image_error.json
index e327a0baa..3c1821026 100644
--- a/tests/data/ngff_examples/image_error.json
+++ b/tests/data/ngff_examples/image_error.json
@@ -9,7 +9,7 @@
             ],
             "datasets": [
                 {
-                    "path": 0,
+                    "path": "0",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -18,7 +18,7 @@
                     ]
                 },
                 {
-                    "path": 1,
+                    "path": "1",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -27,7 +27,7 @@
                     ]
                 },
                 {
-                    "path": 2,
+                    "path": "2",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -36,7 +36,7 @@
                     ]
                 },
                 {
-                    "path": 3,
+                    "path": "3",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -45,7 +45,7 @@
                     ]
                 },
                 {
-                    "path": 4,
+                    "path": "4",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -65,7 +65,7 @@
             ],
             "datasets": [
                 {
-                    "path": 1,
+                    "path": "1_0",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -74,7 +74,7 @@
                     ]
                 },
                 {
-                    "path": 1,
+                    "path": "1_1",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -83,7 +83,7 @@
                     ]
                 },
                 {
-                    "path": 2,
+                    "path": "1_2",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -92,7 +92,7 @@
                     ]
                 },
                 {
-                    "path": 3,
+                    "path": "1_3",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -101,7 +101,7 @@
                     ]
                 },
                 {
-                    "path": 4,
+                    "path": "1_4",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
diff --git a/tests/data/ngff_examples/image_error_coarsening_1.json b/tests/data/ngff_examples/image_error_coarsening_1.json
index 7f11ba9fd..f7341d2f5 100644
--- a/tests/data/ngff_examples/image_error_coarsening_1.json
+++ b/tests/data/ngff_examples/image_error_coarsening_1.json
@@ -24,7 +24,7 @@
             ],
             "datasets": [
                 {
-                    "path": 0,
+                    "path": "0",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -38,7 +38,7 @@
                     ]
                 },
                 {
-                    "path": 1,
+                    "path": "1",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -52,7 +52,7 @@
                     ]
                 },
                 {
-                    "path": 2,
+                    "path": "2",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
diff --git a/tests/data/ngff_examples/image_error_coarsening_2.json b/tests/data/ngff_examples/image_error_coarsening_2.json
index 085a48014..4e9fb640c 100644
--- a/tests/data/ngff_examples/image_error_coarsening_2.json
+++ b/tests/data/ngff_examples/image_error_coarsening_2.json
@@ -24,7 +24,7 @@
             ],
             "datasets": [
                 {
-                    "path": 0,
+                    "path": "0",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -38,7 +38,7 @@
                     ]
                 },
                 {
-                    "path": 1,
+                    "path": "1",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -52,7 +52,7 @@
                     ]
                 },
                 {
-                    "path": 2,
+                    "path": "2",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
diff --git a/tests/data/ngff_examples/image_error_pixels.json b/tests/data/ngff_examples/image_error_pixels.json
index 44130ff46..87c72e1ee 100644
--- a/tests/data/ngff_examples/image_error_pixels.json
+++ b/tests/data/ngff_examples/image_error_pixels.json
@@ -24,7 +24,7 @@
             ],
             "datasets": [
                 {
-                    "path": 0,
+                    "path": "0",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
diff --git a/tests/data/ngff_examples/image_no_omero_channel_window.json b/tests/data/ngff_examples/image_no_omero_channel_window.json
index 8bfebb209..958ead3a7 100644
--- a/tests/data/ngff_examples/image_no_omero_channel_window.json
+++ b/tests/data/ngff_examples/image_no_omero_channel_window.json
@@ -15,7 +15,7 @@
             ],
             "datasets": [
                 {
-                    "path": 0,
+                    "path": "0",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
diff --git a/tests/data/ngff_examples/multiscale.json b/tests/data/ngff_examples/multiscale.json
index 5a722aac9..b2b7b7472 100644
--- a/tests/data/ngff_examples/multiscale.json
+++ b/tests/data/ngff_examples/multiscale.json
@@ -22,7 +22,7 @@
             ],
             "datasets": [
                 {
-                    "path": 0,
+                    "path": "0",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -36,7 +36,7 @@
                     ]
                 },
                 {
-                    "path": 1,
+                    "path": "1",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -50,7 +50,7 @@
                     ]
                 },
                 {
-                    "path": 2,
+                    "path": "2",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -64,7 +64,7 @@
                     ]
                 },
                 {
-                    "path": 3,
+                    "path": "3",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -78,7 +78,7 @@
                     ]
                 },
                 {
-                    "path": 4,
+                    "path": "4",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
diff --git a/tests/data/ngff_examples/multiscale_error.json b/tests/data/ngff_examples/multiscale_error.json
index 5e8d119de..100dd5d36 100644
--- a/tests/data/ngff_examples/multiscale_error.json
+++ b/tests/data/ngff_examples/multiscale_error.json
@@ -33,7 +33,7 @@
             ],
             "datasets": [
                 {
-                    "path": 0,
+                    "path": "0",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -47,7 +47,7 @@
                     ]
                 },
                 {
-                    "path": 1,
+                    "path": "1",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -61,7 +61,7 @@
                     ]
                 },
                 {
-                    "path": 2,
+                    "path": "2",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -75,7 +75,7 @@
                     ]
                 },
                 {
-                    "path": 3,
+                    "path": "3",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -89,7 +89,7 @@
                     ]
                 },
                 {
-                    "path": 4,
+                    "path": "4",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
diff --git a/tests/data/ngff_examples/multiscale_non_unique_axis.json b/tests/data/ngff_examples/multiscale_non_unique_axis.json
new file mode 100644
index 000000000..78cd91138
--- /dev/null
+++ b/tests/data/ngff_examples/multiscale_non_unique_axis.json
@@ -0,0 +1,39 @@
+        {
+            "axes": [
+                {
+                    "name": "c",
+                    "type": "channel"
+                },
+                {
+                    "name": "c",
+                    "type": "channel"
+                },
+                {
+                    "name": "y",
+                    "type": "space",
+                    "unit": "micrometer"
+                },
+                {
+                    "name": "x",
+                    "type": "space",
+                    "unit": "micrometer"
+                }
+            ],
+            "datasets": [
+                {
+                    "path": "0",
+                    "coordinateTransformations": [
+                        {
+                            "type": "scale",
+                            "scale": [
+                                1.0,
+                                1.0,
+                                0.1625,
+                                0.1625
+                            ]
+                        }
+                    ]
+                }
+            ],
+            "version": "0.4"
+        }
diff --git a/tests/data/ngff_examples/multiscale_with_none.json b/tests/data/ngff_examples/multiscale_with_none.json
new file mode 100644
index 000000000..13bbfe06d
--- /dev/null
+++ b/tests/data/ngff_examples/multiscale_with_none.json
@@ -0,0 +1,97 @@
+        {
+            "coordinateTransformations": null,
+            "axes": [
+                {
+                    "name": "c",
+                    "type": "channel"
+                },
+                {
+                    "name": "z",
+                    "type": "space",
+                    "unit": "micrometer"
+                },
+                {
+                    "name": "y",
+                    "type": "space",
+                    "unit": "micrometer"
+                },
+                {
+                    "name": "x",
+                    "type": "space",
+                    "unit": "micrometer"
+                }
+            ],
+            "datasets": [
+                {
+                    "path": "0",
+                    "coordinateTransformations": [
+                        {
+                            "type": "scale",
+                            "scale": [
+                                1.0,
+                                1.0,
+                                0.1625,
+                                0.1625
+                            ]
+                        }
+                    ]
+                },
+                {
+                    "path": "1",
+                    "coordinateTransformations": [
+                        {
+                            "type": "scale",
+                            "scale": [
+                                1.0,
+                                1.0,
+                                0.325,
+                                0.325
+                            ]
+                        }
+                    ]
+                },
+                {
+                    "path": "2",
+                    "coordinateTransformations": [
+                        {
+                            "type": "scale",
+                            "scale": [
+                                1.0,
+                                1.0,
+                                0.65,
+                                0.65
+                            ]
+                        }
+                    ]
+                },
+                {
+                    "path": "3",
+                    "coordinateTransformations": [
+                        {
+                            "type": "scale",
+                            "scale": [
+                                1.0,
+                                1.0,
+                                1.3,
+                                1.3
+                            ]
+                        }
+                    ]
+                },
+                {
+                    "path": "4",
+                    "coordinateTransformations": [
+                        {
+                            "type": "scale",
+                            "scale": [
+                                1.0,
+                                1.0,
+                                2.6,
+                                2.6
+                            ]
+                        }
+                    ]
+                }
+            ],
+            "version": "0.4"
+        }
diff --git a/tests/data/plate_ones.zarr/B/03/0/.zattrs b/tests/data/plate_ones.zarr/B/03/0/.zattrs
index ed7b031cc..db9eec1f8 100644
--- a/tests/data/plate_ones.zarr/B/03/0/.zattrs
+++ b/tests/data/plate_ones.zarr/B/03/0/.zattrs
@@ -24,7 +24,7 @@
             ],
             "datasets": [
                 {
-                    "path": 0,
+                    "path": "0",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -38,7 +38,7 @@
                     ]
                 },
                 {
-                    "path": 1,
+                    "path": "1",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -52,7 +52,7 @@
                     ]
                 },
                 {
-                    "path": 2,
+                    "path": "2",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -66,7 +66,7 @@
                     ]
                 },
                 {
-                    "path": 3,
+                    "path": "3",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
@@ -80,7 +80,7 @@
                     ]
                 },
                 {
-                    "path": 4,
+                    "path": "4",
                     "coordinateTransformations": [
                         {
                             "type": "scale",
diff --git a/tests/data/plate_ones.zarr/B/03/0/tables/.zattrs b/tests/data/plate_ones.zarr/B/03/0/tables/.zattrs
new file mode 100644
index 000000000..724424284
--- /dev/null
+++ b/tests/data/plate_ones.zarr/B/03/0/tables/.zattrs
@@ -0,0 +1,5 @@
+{
+    "tables": [
+        "FOV_ROI_table"
+    ]
+}
diff --git a/tests/data/plate_ones.zarr/B/03/0/tables/FOV_ROI_table/.zattrs b/tests/data/plate_ones.zarr/B/03/0/tables/FOV_ROI_table/.zattrs
index 433b8f0f6..f4e008769 100644
--- a/tests/data/plate_ones.zarr/B/03/0/tables/FOV_ROI_table/.zattrs
+++ b/tests/data/plate_ones.zarr/B/03/0/tables/FOV_ROI_table/.zattrs
@@ -1,6 +1,6 @@
 {
-    "fractal_table_version": "1",
-    "type": "roi_table",
     "encoding-type": "anndata",
-    "encoding-version": "0.1.0"
+    "encoding-version": "0.1.0",
+    "fractal_table_version": "1",
+    "type": "roi_table"
 }
diff --git a/tests/dev/test_create_schema_for_single_task.py b/tests/dev/test_create_schema_for_single_task.py
index afc4c307a..2cd43ba00 100644
--- a/tests/dev/test_create_schema_for_single_task.py
+++ b/tests/dev/test_create_schema_for_single_task.py
@@ -2,7 +2,7 @@
 
 import pytest
 from devtools import debug
-from pydantic.v1.decorator import validate_arguments
+from pydantic import validate_call
 
 from fractal_tasks_core.dev.lib_args_schemas import (
     create_schema_for_single_task,
@@ -23,7 +23,7 @@ def test_create_schema_for_single_task_usage_1():
     print(json.dumps(schema, indent=2))
 
 
-@validate_arguments
+@validate_call
 def task_function(arg_1: int = 1):
     """
     Description
diff --git a/tests/dev/test_enum_arguments.py b/tests/dev/test_enum_arguments.py
index a003217b1..0a2b7169b 100644
--- a/tests/dev/test_enum_arguments.py
+++ b/tests/dev/test_enum_arguments.py
@@ -2,7 +2,7 @@
 from enum import Enum
 
 from devtools import debug
-from pydantic.v1.decorator import validate_arguments
+from pydantic import validate_call
 
 from fractal_tasks_core.dev.lib_args_schemas import (
     create_schema_for_single_task,
@@ -21,7 +21,7 @@ class ColorA(Enum):
 )
 
 
-@validate_arguments
+@validate_call
 def task_function(
     arg_A: ColorA,
     arg_B: ColorB,
diff --git a/tests/dev/test_optional_argument.py b/tests/dev/test_optional_argument.py
new file mode 100644
index 000000000..0d2f764a9
--- /dev/null
+++ b/tests/dev/test_optional_argument.py
@@ -0,0 +1,48 @@
+import json
+from typing import Optional
+
+from pydantic.validate_call_decorator import validate_call
+
+from fractal_tasks_core.dev.lib_args_schemas import (
+    create_schema_for_single_task,
+)
+
+
+@validate_call
+def task_function(
+    arg1: str,
+    arg2: Optional[str] = None,
+    arg3: Optional[list[str]] = None,
+):
+    """
+    Short task description
+
+    Args:
+        arg1: This is the argument description
+        arg2: This is the argument description
+        arg3: This is the argument description
+    """
+    pass
+
+
+def test_optional_argument():
+    """
+    As a first implementation of the Pydantic V2 schema generation, we are not
+    supporting the `anyOf` pattern for nullable attributes. This test verifies
+    that the type of nullable properties is not `anyOf`, and that they are not
+    required.
+
+    Note: future versions of fractal-tasks-core may change this behavior.
+    """
+    schema = create_schema_for_single_task(
+        task_function=validate_call(task_function),
+        executable=__file__,
+        package=None,
+        verbose=True,
+    )
+    print(json.dumps(schema, indent=2, sort_keys=True))
+    print()
+    assert schema["properties"]["arg2"]["type"] == "string"
+    assert "arg2" not in schema["required"]
+    assert schema["properties"]["arg3"]["type"] == "array"
+    assert "arg3" not in schema["required"]
diff --git a/tests/dev/test_tuple_argument.py b/tests/dev/test_tuple_argument.py
new file mode 100644
index 000000000..213abba52
--- /dev/null
+++ b/tests/dev/test_tuple_argument.py
@@ -0,0 +1,36 @@
+import json
+
+from devtools import debug
+from pydantic import validate_call
+
+from fractal_tasks_core.dev.lib_args_schemas import (
+    create_schema_for_single_task,
+)
+
+
+@validate_call
+def task_function(arg_A: tuple[int, int] = (1, 1)):
+    """
+    Short task description
+
+    Args:
+        arg_A: Description of arg_A.
+    """
+    pass
+
+
+def test_tuple_argument():
+    """
+    This test only asserts that `create_schema_for_single_task` runs
+    successfully. Its goal is also to offer a quick way to experiment
+    with new task arguments and play with the generated JSON Schema,
+    without re-building the whole fractal-tasks-core manifest.
+    """
+    schema = create_schema_for_single_task(
+        task_function=task_function,
+        executable=__file__,
+        package=None,
+        verbose=True,
+    )
+    debug(schema)
+    print(json.dumps(schema, indent=2))
diff --git a/tests/tasks/test_executable_workflow.py b/tests/tasks/test_executable_workflow.py
index 1cd5d5b6a..99ea224f1 100644
--- a/tests/tasks/test_executable_workflow.py
+++ b/tests/tasks/test_executable_workflow.py
@@ -51,7 +51,6 @@ def run_command(cmd: str):
 
 
 def test_workflow_yokogawa_to_ome_zarr(tmp_path: Path, zenodo_images: str):
-
     # Init
     img_path = zenodo_images
     zarr_dir = str(tmp_path / "tmp_out/")
diff --git a/tests/tasks/test_unit_napari_workflows_wrapper.py b/tests/tasks/test_unit_napari_workflows_wrapper.py
index 616ba9a1d..1f88ea4e6 100644
--- a/tests/tasks/test_unit_napari_workflows_wrapper.py
+++ b/tests/tasks/test_unit_napari_workflows_wrapper.py
@@ -2,7 +2,7 @@
 
 import pytest
 from devtools import debug
-from pydantic.v1.error_wrappers import ValidationError
+from pydantic import ValidationError
 
 from fractal_tasks_core.tasks.napari_workflows_wrapper import (
     napari_workflows_wrapper,
diff --git a/tests/tasks/test_valid_args_schemas.py b/tests/tasks/test_valid_args_schemas.py
index 0bbd7a298..9bc8818f1 100644
--- a/tests/tasks/test_valid_args_schemas.py
+++ b/tests/tasks/test_valid_args_schemas.py
@@ -191,7 +191,7 @@ def test_args_title():
     new_schema = create_schema_for_single_task(
         cellvoyager_task["executable_non_parallel"]
     )
-    definitions = new_schema["definitions"]
+    definitions = new_schema["$defs"]
     omero_channel_def = definitions["OmeroChannel"]
     # Custom-model-typed attribute of custom-model-typed task argument
     window_prop = omero_channel_def["properties"]["window"]
diff --git a/tests/tasks/test_valid_task_interface.py b/tests/tasks/test_valid_task_interface.py
index 3a75f2e23..661e44aa4 100644
--- a/tests/tasks/test_valid_task_interface.py
+++ b/tests/tasks/test_valid_task_interface.py
@@ -26,10 +26,10 @@ def validate_command(cmd: str):
     debug(stderr)
     # Valid stderr includes pydantic.v1.error_wrappers.ValidationError (type
     # match between model and function, but tmp_file_args has wrong arguments)
-    assert "pydantic.v1.error_wrappers.ValidationError" in stderr
-    # Valid stderr must include a mention of "unexpected keyword arguments",
+    assert "ValidationError" in stderr
+    # Valid stderr must include a mention of "Unexpected keyword argument",
     # because we are including some invalid arguments
-    assert "unexpected keyword arguments" in stderr
+    assert "Unexpected keyword argument" in stderr
     # Invalid stderr includes ValueError
     assert "ValueError" not in stderr
 
@@ -41,7 +41,6 @@ def validate_command(cmd: str):
 
 @pytest.mark.parametrize("task", manifest_dict["task_list"])
 def test_task_interface(task, tmp_path):
-
     tmp_file_args = str(tmp_path / "args.json")
     tmp_file_metadiff = str(tmp_path / "metadiff.json")
     with open(tmp_file_args, "w") as fout:
diff --git a/tests/tasks/test_workflows_cellpose_segmentation.py b/tests/tasks/test_workflows_cellpose_segmentation.py
index 2bf336d90..ff9a70e55 100644
--- a/tests/tasks/test_workflows_cellpose_segmentation.py
+++ b/tests/tasks/test_workflows_cellpose_segmentation.py
@@ -38,6 +38,9 @@
 from fractal_tasks_core.tasks.cellpose_utils import (
     CellposeChannel1InputModel,
 )
+from fractal_tasks_core.tasks.cellpose_utils import (
+    CellposeChannel2InputModel,
+)
 from fractal_tasks_core.tasks.cellpose_utils import (
     CellposeCustomNormalizer,
 )
@@ -126,7 +129,6 @@ def patched_segment_ROI_no_labels(
 def patched_segment_ROI_overlapping_organoids(
     x, label_dtype=None, well_id=None, **kwargs
 ):
-
     import logging
 
     logger = logging.getLogger("cellpose_segmentation.py")
@@ -170,7 +172,6 @@ def test_failures(
     caplog: pytest.LogCaptureFixture,
     monkeypatch: MonkeyPatch,
 ):
-
     monkeypatch.setattr(
         "fractal_tasks_core.tasks.cellpose_segmentation.cellpose.core.use_gpu",
         patched_cellpose_core_use_gpu,
@@ -240,7 +241,6 @@ def test_workflow_with_per_FOV_labeling(
     caplog: pytest.LogCaptureFixture,
     monkeypatch: MonkeyPatch,
 ):
-
     monkeypatch.setattr(
         "fractal_tasks_core.tasks.cellpose_segmentation.cellpose.core.use_gpu",
         patched_cellpose_core_use_gpu,
@@ -332,7 +332,7 @@ def test_workflow_with_multi_channel_input(
     channel = CellposeChannel1InputModel(
         wavelength_id="A01_C01", normalize=CellposeCustomNormalizer()
     )
-    channel2 = CellposeChannel1InputModel(
+    channel2 = CellposeChannel2InputModel(
         wavelength_id="A01_C01", normalize=CellposeCustomNormalizer()
     )
     for zarr_url in zarr_urls:
@@ -364,7 +364,6 @@ def test_workflow_with_per_FOV_labeling_2D(
     zenodo_zarr: list[str],
     monkeypatch: MonkeyPatch,
 ):
-
     monkeypatch.setattr(
         "fractal_tasks_core.tasks.cellpose_segmentation.cellpose.core.use_gpu",
         patched_cellpose_core_use_gpu,
@@ -414,7 +413,6 @@ def test_workflow_with_per_well_labeling_2D(
     zenodo_images: str,
     monkeypatch: MonkeyPatch,
 ):
-
     monkeypatch.setattr(
         "fractal_tasks_core.tasks.cellpose_segmentation.cellpose.core.use_gpu",
         patched_cellpose_core_use_gpu,
@@ -506,7 +504,6 @@ def test_workflow_bounding_box(
     caplog: pytest.LogCaptureFixture,
     monkeypatch: MonkeyPatch,
 ):
-
     monkeypatch.setattr(
         "fractal_tasks_core.tasks.cellpose_segmentation.cellpose.core.use_gpu",
         patched_cellpose_core_use_gpu,
@@ -591,7 +588,6 @@ def test_workflow_bounding_box_with_overlap(
     caplog: pytest.LogCaptureFixture,
     monkeypatch: MonkeyPatch,
 ):
-
     monkeypatch.setattr(
         "fractal_tasks_core.tasks.cellpose_segmentation.cellpose.core.use_gpu",
         patched_cellpose_core_use_gpu,
@@ -783,21 +779,21 @@ def test_workflow_with_per_FOV_labeling_via_script(
     with args_path.open("w") as f:
         json.dump(this_task_args, f, indent=2)
     res = subprocess.run(shlex.split(command), **run_options)  # type: ignore
-    print(res.stdout)
-    print(res.stderr)
+    debug(res.stdout)
+    debug(res.stderr)
     # If this check fails after updating the cellpose version, you'll likely
     # need to update the manifest to include a changed set of available models
     # See https://github.com/fractal-analytics-platform/fractal-tasks-core/issues/401 # noqa E501
     error_msg = (
-        "unexpected value; permitted: 'cyto', 'nuclei', "
-        "'tissuenet', 'livecell', 'cyto2', 'general', 'CP', 'CPx', "
-        "'TN1', 'TN2', 'TN3', 'LC1', 'LC2', 'LC3', 'LC4' "
-        f"(type=value_error.const; given={INVALID_MODEL_TYPE}; "
-        "permitted=('cyto', 'nuclei', 'tissuenet', 'livecell', "
-        "'cyto2', 'general', 'CP', 'CPx', 'TN1', 'TN2', 'TN3', "
-        "'LC1', 'LC2', 'LC3', 'LC4'))"
+        "Input should be 'cyto', 'nuclei', 'tissuenet', 'livecell', "
+        "'cyto2', 'general', 'CP', 'CPx', 'TN1', 'TN2', 'TN3', 'LC1', "
+        "'LC2', 'LC3' or 'LC4' [type=literal_error, "
+        f"input_value='{INVALID_MODEL_TYPE}', input_type=str]"
     )
+    print(res.stderr)
+    print(error_msg)
     assert error_msg in res.stderr
+    # assert error_msg in res.stderr
     assert "urllib.error.HTTPError" not in res.stdout
     assert "urllib.error.HTTPError" not in res.stderr
 
@@ -916,7 +912,6 @@ def test_workflow_secondary_labeling(
     zenodo_zarr: list[str],
     monkeypatch: MonkeyPatch,
 ):
-
     monkeypatch.setattr(
         "fractal_tasks_core.tasks.cellpose_segmentation.cellpose.core.use_gpu",
         patched_cellpose_core_use_gpu,
@@ -1121,11 +1116,15 @@ def test_workflow_secondary_labeling_two_channels(
     debug(zarr_urls[0])
 
     # Secondary segmentation (nuclei)
+    channel2 = CellposeChannel2InputModel(
+        wavelength_id=channel.wavelength_id, normalize=channel.normalize
+    )
+
     for zarr_url in zarr_urls:
         cellpose_segmentation(
             zarr_url=zarr_url,
             channel=channel,
-            channel2=channel,
+            channel2=channel2,
             level=0,
             relabeling=True,
             input_ROI_table="organoid_ROI_table",
diff --git a/tests/test_unit_channels.py b/tests/test_unit_channels.py
index 501b53f07..76fe38dcc 100644
--- a/tests/test_unit_channels.py
+++ b/tests/test_unit_channels.py
@@ -52,7 +52,7 @@ def test_check_well_channel_labels(tmp_path):
                 channels=[
                     OmeroChannel(
                         wavelength_id="id_1", label="non_unique_label"
-                    ).dict(),
+                    ).model_dump(),
                 ]
             )
         )
@@ -63,7 +63,7 @@ def test_check_well_channel_labels(tmp_path):
                 channels=[
                     OmeroChannel(
                         wavelength_id="id_1", label="non_unique_label"
-                    ).dict(),
+                    ).model_dump(),
                 ]
             )
         )
@@ -78,7 +78,6 @@ def test_check_well_channel_labels(tmp_path):
 
 
 def test_get_channel_from_list(testdata_path: Path):
-
     # Read JSON data and cast into `OmeroChannel`s
     with (testdata_path / "omero/channels_list.json").open("r") as f:
         omero_channels_dict = json.load(f)
@@ -227,7 +226,6 @@ def test_color_validator():
     ],
 )
 def test_update_omero_channels(old_channels):
-
     # Update partial metadata
     print()
     print(f"OLD: {old_channels}")
diff --git a/tests/test_unit_input_models.py b/tests/test_unit_input_models.py
index b74ba4251..89cbe8f96 100644
--- a/tests/test_unit_input_models.py
+++ b/tests/test_unit_input_models.py
@@ -11,7 +11,6 @@
 
 
 def test_Channel():
-
     # Valid
 
     c = ChannelInputModel(wavelength_id="wavelength_id")
@@ -36,7 +35,6 @@ def test_Channel():
 
 
 def test_NapariWorkflowsInput():
-
     # Invalid
 
     with pytest.raises(ValueError) as e:
@@ -65,7 +63,6 @@ def test_NapariWorkflowsInput():
 
 
 def test_NapariWorkflowsOutput():
-
     # Invalid
 
     with pytest.raises(ValueError) as e:
diff --git a/tests/test_unit_ngff.py b/tests/test_unit_ngff.py
index e2b346553..a4bd21c67 100644
--- a/tests/test_unit_ngff.py
+++ b/tests/test_unit_ngff.py
@@ -73,13 +73,23 @@ def test_Dataset(ngffdata_path):
 
 
 def test_Multiscale(ngffdata_path):
+
     # Fail due to global coordinateTransformation
     with pytest.raises(NotImplementedError):
         _load_and_validate(ngffdata_path / "multiscale_error.json", Multiscale)
 
-    # Success
+    # Fail due to non-unique axis
+    with pytest.raises(ValueError):
+        _load_and_validate(
+            ngffdata_path / "multiscale_non_unique_axis.json", Multiscale
+        )
+
+    # Success with no global `coordinateTransformations`
     _load_and_validate(ngffdata_path / "multiscale.json", Multiscale)
 
+    # Success with `None` global `coordinateTransformations`
+    _load_and_validate(ngffdata_path / "multiscale_with_none.json", Multiscale)
+
 
 def test_NgffImageMeta(ngffdata_path):