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.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/manifest_external_packages.yml b/.github/workflows/manifest_external_packages.yml
index e4a471d72..aea0bbddc 100644
--- a/.github/workflows/manifest_external_packages.yml
+++ b/.github/workflows/manifest_external_packages.yml
@@ -75,9 +75,23 @@ jobs:
       - name: Install package
         run:  ${{ matrix.cmd_install }}
 
+      - name: Get 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: DEBUG install devtools
+        run: python -m pip install devtools
+
       - 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 }}
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2e0d192e4..caba141e3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,8 +8,13 @@
     * 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).
-* Development:
-    * Switch all core models to pydantic v2 (\#790).
+* 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.0
 
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/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 8f86aeca0..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:
diff --git a/fractal_tasks_core/dev/lib_args_schemas.py b/fractal_tasks_core/dev/lib_args_schemas.py
index a28a5429e..5b8ae5c01 100644
--- a/fractal_tasks_core/dev/lib_args_schemas.py
+++ b/fractal_tasks_core/dev/lib_args_schemas.py
@@ -15,17 +15,20 @@
 import logging
 import os
 from collections import Counter
+from copy import deepcopy
 from pathlib import Path
 from typing import Any
 from typing import Callable
 from typing import Optional
 
+from devtools import debug
 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 pydantic.json_schema import GenerateJsonSchema
+from pydantic.json_schema import JsonSchemaValue
+from pydantic_core.core_schema import WithDefaultSchema
 
 from fractal_tasks_core.dev.lib_descriptions import (
     _get_class_attrs_descriptions,
@@ -48,6 +51,7 @@
 
 _Schema = dict[str, Any]
 
+
 FRACTAL_TASKS_CORE_PYDANTIC_MODELS = [
     ("fractal_tasks_core", "channels.py", "OmeroChannel"),
     ("fractal_tasks_core", "channels.py", "Window"),
@@ -90,58 +94,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 +113,114 @@ 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
 
 
+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:
+            debug("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:
+            debug("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:
+        # Inspired by
+        # https://github.com/vitalik/django-ninja/issues/842#issuecomment-2059014537
+        original_json_schema_value = super().get_flattened_anyof(schemas)
+        members = original_json_schema_value.get("anyOf")
+        debug("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]}
+            debug("C", new_json_schema_value)
+            return new_json_schema_value
+        else:
+            return original_json_schema_value
+
+
+class GenerateJsonSchemaD(GenerateJsonSchema):
+    def get_flattened_anyof(
+        self, schemas: list[JsonSchemaValue]
+    ) -> JsonSchemaValue:
+        # Inspired by
+        # https://github.com/vitalik/django-ninja/issues/842#issuecomment-2059014537
+        null_schema = {"type": "null"}
+        if null_schema in schemas:
+            debug("D drop null_schema before calling `get_flattened_anyof`")
+            schemas.pop(schemas.index(null_schema))
+        return super().get_flattened_anyof(schemas)
+
+
+class GenerateJsonSchemaE(GenerateJsonSchemaD):
+    def default_schema(self, schema: WithDefaultSchema) -> JsonSchemaValue:
+        json_schema = super().default_schema(schema)
+        debug("E", json_schema)
+        if "default" in json_schema.keys() and json_schema["default"] is None:
+            debug("E: Pop None default")
+            json_schema.pop("default")
+        return json_schema
+
+
+CustomGenerateJsonSchema = GenerateJsonSchema
+CustomGenerateJsonSchema = GenerateJsonSchemaA
+CustomGenerateJsonSchema = GenerateJsonSchemaB
+CustomGenerateJsonSchema = GenerateJsonSchemaC
+CustomGenerateJsonSchema = GenerateJsonSchemaD
+CustomGenerateJsonSchema = GenerateJsonSchemaE
+
+
+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 +238,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 +294,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 +320,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 +354,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_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..4f07a9b11 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):
diff --git a/poetry.lock b/poetry.lock
index 6a581792f..7435e12af 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
 
 [[package]]
 name = "alabaster"
@@ -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 = "4915d9c487a3591587557828482b4b83474d14f3130124732918570691f7ee85"
+content-hash = "824fe40b5aa9a15003ffcda259d6bdd9cd45e193604497127264d59eb0e84106"
diff --git a/pyproject.toml b/pyproject.toml
index 78a54808e..4b1c4f2fd 100755
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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/dev/test_optional_argument.py b/tests/dev/test_optional_argument.py
new file mode 100644
index 000000000..35ddc36db
--- /dev/null
+++ b/tests/dev/test_optional_argument.py
@@ -0,0 +1,46 @@
+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.
+    """
+    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/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"]