diff --git a/lib/modules/datasource/packagist/index.spec.ts b/lib/modules/datasource/packagist/index.spec.ts
index a0c7b09fc9f92ba9aa2731eb58beea5732b772e0..4ef75442e11e33227c2a323606f98ed25188319f 100644
--- a/lib/modules/datasource/packagist/index.spec.ts
+++ b/lib/modules/datasource/packagist/index.spec.ts
@@ -82,7 +82,12 @@ describe('modules/datasource/packagist/index', () => {
         .scope('https://composer.renovatebot.com')
         .get('/packages.json')
         .replyWithError({ code: 'ETIMEDOUT' });
-      httpMock.scope(baseUrl).get('/p2/vendor/package-name2.json').reply(200);
+      httpMock
+        .scope(baseUrl)
+        .get('/p2/vendor/package-name2.json')
+        .reply(200)
+        .get('/p2/vendor/package-name2~dev.json')
+        .reply(200);
       const res = await getPkgReleases({
         ...config,
         datasource,
@@ -97,7 +102,12 @@ describe('modules/datasource/packagist/index', () => {
         .scope('https://composer.renovatebot.com')
         .get('/packages.json')
         .reply(403);
-      httpMock.scope(baseUrl).get('/p2/vendor/package-name.json').reply(200);
+      httpMock
+        .scope(baseUrl)
+        .get('/p2/vendor/package-name.json')
+        .reply(200)
+        .get('/p2/vendor/package-name~dev.json')
+        .reply(200);
       const res = await getPkgReleases({
         ...config,
         datasource,
@@ -112,7 +122,12 @@ describe('modules/datasource/packagist/index', () => {
         .scope('https://composer.renovatebot.com')
         .get('/packages.json')
         .reply(404);
-      httpMock.scope(baseUrl).get('/p2/drewm/mailchimp-api.json').reply(200);
+      httpMock
+        .scope(baseUrl)
+        .get('/p2/drewm/mailchimp-api.json')
+        .reply(200)
+        .get('/p2/drewm/mailchimp-api~dev.json')
+        .reply(200);
       const res = await getPkgReleases({
         ...config,
         datasource,
@@ -266,7 +281,12 @@ describe('modules/datasource/packagist/index', () => {
           '/p/providers-2018-09$14346045d7a7261cb3a12a6b7a1a7c4151982530347b115e5e277d879cad1942.json'
         )
         .reply(200, fileJson);
-      httpMock.scope(baseUrl).get('/p2/some/other.json').reply(200, beytJson);
+      httpMock
+        .scope(baseUrl)
+        .get('/p2/some/other.json')
+        .reply(200, beytJson)
+        .get('/p2/some/other~dev.json')
+        .reply(200, beytJson);
       const res = await getPkgReleases({
         ...config,
         datasource,
@@ -357,7 +377,12 @@ describe('modules/datasource/packagist/index', () => {
         .scope('https://composer.renovatebot.com')
         .get('/packages.json')
         .reply(200, packagesJson);
-      httpMock.scope(baseUrl).get('/p2/some/other.json').reply(200, beytJson);
+      httpMock
+        .scope(baseUrl)
+        .get('/p2/some/other.json')
+        .reply(200, beytJson)
+        .get('/p2/some/other~dev.json')
+        .reply(200, beytJson);
       const res = await getPkgReleases({
         ...config,
         datasource,
diff --git a/lib/modules/datasource/packagist/index.ts b/lib/modules/datasource/packagist/index.ts
index 528941feee8aa3549cdbc572e32ddc9d768dbfdc..9171467dc48c1d6d4b9dcfc22a0014e58d7b20fc 100644
--- a/lib/modules/datasource/packagist/index.ts
+++ b/lib/modules/datasource/packagist/index.ts
@@ -1,7 +1,6 @@
 import URL from 'url';
 import { logger } from '../../../logger';
 import { ExternalHostError } from '../../../types/errors/external-host-error';
-import * as packageCache from '../../../util/cache/package';
 import { cache } from '../../../util/cache/package/decorator';
 import * as hostRules from '../../../util/host-rules';
 import type { HttpOptions } from '../../../util/http/types';
@@ -11,6 +10,7 @@ import { ensureTrailingSlash, joinUrlParts } from '../../../util/url';
 import * as composerVersioning from '../../versioning/composer';
 import { Datasource } from '../datasource';
 import type { GetReleasesConfig, ReleaseResult } from '../types';
+import * as schema from './schema';
 import type {
   AllPackages,
   PackageMeta,
@@ -195,35 +195,19 @@ export class PackagistDatasource extends Datasource {
     return allPackages;
   }
 
+  @cache({
+    namespace: `datasource-${PackagistDatasource.id}-org`,
+    key: (regUrl: string) => regUrl,
+    ttlMinutes: 10,
+  })
   async packagistOrgLookup(name: string): Promise<ReleaseResult | null> {
-    const cacheNamespace = 'datasource-packagist-org';
-    const cachedResult = await packageCache.get<ReleaseResult>(
-      cacheNamespace,
-      name
-    );
-    // istanbul ignore if
-    if (cachedResult) {
-      return cachedResult;
-    }
-    let dep: ReleaseResult | null = null;
     const regUrl = 'https://packagist.org';
-    const pkgUrl = [
-      joinUrlParts(regUrl, `/p2/${name}.json`),
-      joinUrlParts(regUrl, `/p2/${name}~dev.json`),
-    ];
-    // TODO: fix types (#9610)
-    let res = (await this.http.getJson<any>(pkgUrl[0])).body.packages[name];
-    res = [
-      ...res,
-      ...(await this.http.getJson<any>(pkgUrl[1])).body.packages[name],
-    ];
-    if (res) {
-      dep = PackagistDatasource.extractDepReleases(res);
-      logger.trace({ dep }, 'dep');
-    }
-    const cacheMinutes = 10;
-    await packageCache.set(cacheNamespace, name, dep, cacheMinutes);
-    return dep;
+    const pkgUrl = joinUrlParts(regUrl, `/p2/${name}.json`);
+    const devUrl = joinUrlParts(regUrl, `/p2/${name}~dev.json`);
+    const results = await p.map([pkgUrl, devUrl], (url) =>
+      this.http.getJson(url).then(({ body }) => body)
+    );
+    return schema.parsePackagesResponses(name, results);
   }
 
   public override async getReleases({
diff --git a/lib/modules/datasource/packagist/schema.spec.ts b/lib/modules/datasource/packagist/schema.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..679d5c14713e3e9661b614af36d4e82bc4964b53
--- /dev/null
+++ b/lib/modules/datasource/packagist/schema.spec.ts
@@ -0,0 +1,154 @@
+import {
+  ComposerRelease,
+  ComposerReleaseArray,
+  parsePackagesResponse,
+  parsePackagesResponses,
+} from './schema';
+
+describe('modules/datasource/packagist/schema', () => {
+  describe('ComposerRelease', () => {
+    it('rejects', () => {
+      expect(() => ComposerRelease.parse(null)).toThrow();
+      expect(() => ComposerRelease.parse(undefined)).toThrow();
+      expect(() => ComposerRelease.parse('')).toThrow();
+      expect(() => ComposerRelease.parse({})).toThrow();
+      expect(() => ComposerRelease.parse({ version: null })).toThrow();
+      expect(() => ComposerRelease.parse({ version: null })).toThrow();
+      expect(() => ComposerRelease.parse({ version: '' })).toThrow();
+      expect(() => ComposerRelease.parse({ version: 'dev-main' })).toThrow();
+    });
+
+    it('parses', () => {
+      const defaultResult = {
+        version: '1.2.3',
+        homepage: null,
+        source: null,
+        time: null,
+      };
+
+      expect(ComposerRelease.parse({ version: '1.2.3' })).toEqual(
+        defaultResult
+      );
+
+      expect(ComposerRelease.parse({ version: '1.2.3', homepage: 42 })).toEqual(
+        defaultResult
+      );
+
+      expect(
+        ComposerRelease.parse({ version: '1.2.3', homepage: 'example.com' })
+      ).toEqual({ ...defaultResult, homepage: 'example.com' });
+
+      expect(
+        ComposerRelease.parse({ version: '1.2.3', source: 'nonsense' })
+      ).toEqual(defaultResult);
+
+      expect(
+        ComposerRelease.parse({ version: '1.2.3', source: { url: 'foobar' } })
+      ).toEqual({ ...defaultResult, source: 'foobar' });
+
+      expect(
+        ComposerRelease.parse({ version: '1.2.3', time: '12345' })
+      ).toEqual({ ...defaultResult, time: '12345' });
+    });
+  });
+
+  describe('ComposerReleaseArray', () => {
+    it('rejects', () => {
+      expect(() => ComposerReleaseArray.parse(null)).toThrow();
+      expect(() => ComposerReleaseArray.parse(undefined)).toThrow();
+      expect(() => ComposerReleaseArray.parse('')).toThrow();
+      expect(() => ComposerReleaseArray.parse({})).toThrow();
+    });
+
+    it('parses', () => {
+      expect(ComposerReleaseArray.parse([])).toEqual([]);
+      expect(ComposerReleaseArray.parse([null])).toEqual([]);
+      expect(ComposerReleaseArray.parse([1, 2, 3])).toEqual([]);
+      expect(ComposerReleaseArray.parse(['foobar'])).toEqual([]);
+      expect(ComposerReleaseArray.parse([{ version: '1.2.3' }])).toEqual([
+        { version: '1.2.3', homepage: null, source: null, time: null },
+      ]);
+    });
+  });
+
+  describe('parsePackageResponse', () => {
+    it('parses', () => {
+      expect(parsePackagesResponse('foo/bar', null)).toEqual([]);
+      expect(parsePackagesResponse('foo/bar', {})).toEqual([]);
+      expect(parsePackagesResponse('foo/bar', { packages: '123' })).toEqual([]);
+      expect(parsePackagesResponse('foo/bar', { packages: {} })).toEqual([]);
+      expect(
+        parsePackagesResponse('foo/bar', {
+          packages: {
+            'foo/bar': [{ version: '1.2.3' }],
+            'baz/qux': [{ version: '4.5.6' }],
+          },
+        })
+      ).toEqual([
+        { version: '1.2.3', homepage: null, source: null, time: null },
+      ]);
+    });
+  });
+
+  describe('parsePackagesResponses', () => {
+    it('parses', () => {
+      expect(parsePackagesResponses('foo/bar', [null])).toBeNull();
+      expect(parsePackagesResponses('foo/bar', [{}])).toBeNull();
+      expect(
+        parsePackagesResponses('foo/bar', [{ packages: '123' }])
+      ).toBeNull();
+      expect(parsePackagesResponses('foo/bar', [{ packages: {} }])).toBeNull();
+      expect(
+        parsePackagesResponses('foo/bar', [
+          {
+            packages: {
+              'foo/bar': [
+                {
+                  version: 'v1.1.1',
+                  time: '111',
+                  homepage: 'https://example.com/1',
+                  source: { url: 'git@example.com:foo/bar-1' },
+                },
+              ],
+              'baz/qux': [
+                {
+                  version: 'v2.2.2',
+                  time: '222',
+                  homepage: 'https://example.com/2',
+                  source: { url: 'git@example.com:baz/qux-2' },
+                },
+              ],
+            },
+          },
+          {
+            packages: {
+              'foo/bar': [
+                {
+                  version: 'v3.3.3',
+                  time: '333',
+                  homepage: 'https://example.com/3',
+                  source: { url: 'git@example.com:foo/bar-3' },
+                },
+              ],
+              'baz/qux': [
+                {
+                  version: 'v4.4.4',
+                  time: '444',
+                  homepage: 'https://example.com/4',
+                  source: { url: 'git@example.com:baz/qux-3' },
+                },
+              ],
+            },
+          },
+        ])
+      ).toEqual({
+        homepage: 'https://example.com/3',
+        sourceUrl: 'git@example.com:foo/bar-3',
+        releases: [
+          { version: '1.1.1', gitRef: 'v1.1.1', releaseTimestamp: '111' },
+          { version: '3.3.3', gitRef: 'v3.3.3', releaseTimestamp: '333' },
+        ],
+      });
+    });
+  });
+});
diff --git a/lib/modules/datasource/packagist/schema.ts b/lib/modules/datasource/packagist/schema.ts
new file mode 100644
index 0000000000000000000000000000000000000000..afaacc61d4d019f006ccc04683907ca37bd5732f
--- /dev/null
+++ b/lib/modules/datasource/packagist/schema.ts
@@ -0,0 +1,97 @@
+import { z } from 'zod';
+import { api as versioning } from '../../versioning/composer';
+import type { Release, ReleaseResult } from '../types';
+
+export const ComposerRelease = z.object({
+  version: z
+    .string()
+    .refine((v) => versioning.isSingleVersion(v), 'Invalid version'),
+  homepage: z.string().nullable().catch(null),
+  source: z
+    .object({
+      url: z.string(),
+    })
+    .transform((x) => x.url)
+    .nullable()
+    .catch(null),
+  time: z.string().nullable().catch(null),
+});
+
+export const ComposerReleaseArray = z
+  .array(ComposerRelease.nullable().catch(null))
+  .transform((xs) =>
+    xs.filter((x): x is z.infer<typeof ComposerRelease> => x !== null)
+  );
+export type ComposerReleaseArray = z.infer<typeof ComposerReleaseArray>;
+
+export const ComposerPackagesResponse = z.object({
+  packages: z.record(z.unknown()),
+});
+
+export function parsePackagesResponse(
+  packageName: string,
+  packagesResponse: unknown
+): ComposerReleaseArray {
+  const packagesResponseParsed =
+    ComposerPackagesResponse.safeParse(packagesResponse);
+  if (!packagesResponseParsed.success) {
+    return [];
+  }
+
+  const { packages } = packagesResponseParsed.data;
+  const releaseArray = packages[packageName];
+  const releaseArrayParsed = ComposerReleaseArray.safeParse(releaseArray);
+  if (!releaseArrayParsed.success) {
+    return [];
+  }
+
+  return releaseArrayParsed.data;
+}
+
+export function parsePackagesResponses(
+  packageName: string,
+  packagesResponses: unknown[]
+): ReleaseResult | null {
+  const releases: Release[] = [];
+  let maxVersion: string | null = null;
+  let homepage: string | null = null;
+  let sourceUrl: string | null = null;
+
+  for (const packagesResponse of packagesResponses) {
+    const releaseArray = parsePackagesResponse(packageName, packagesResponse);
+    for (const composerRelease of releaseArray) {
+      const version = composerRelease.version.replace(/^v/, '');
+      const gitRef = composerRelease.version;
+
+      const dep: Release = { version, gitRef };
+
+      if (composerRelease.time) {
+        dep.releaseTimestamp = composerRelease.time;
+      }
+
+      releases.push(dep);
+
+      if (!maxVersion || versioning.isGreaterThan(version, maxVersion)) {
+        maxVersion = version;
+        homepage = composerRelease.homepage;
+        sourceUrl = composerRelease.source;
+      }
+    }
+  }
+
+  if (releases.length === 0) {
+    return null;
+  }
+
+  const result: ReleaseResult = { releases };
+
+  if (homepage) {
+    result.homepage = homepage;
+  }
+
+  if (sourceUrl) {
+    result.sourceUrl = sourceUrl;
+  }
+
+  return result;
+}