Sanity Studio v2 to v3 upgrade: what breaks and how to fix it

By Nayan Kyada · · 7 min read

Part of The Sanity + Next.js Guide

If your team is still on Sanity Studio v2, you are running software that Sanity officially stopped supporting in mid-2024. The Sanity Studio v2 to v3 upgrade is not a minor version bump — the configuration model, plugin API, and custom input system were all rewritten. This post walks through what breaks, what the actual risk is, how to approach the migration, and what questions to ask a developer before hiring someone to do it.

Why v2 is no longer safe to leave alone

Sanity stopped shipping security patches to Studio v2 in 2024. That matters more than it sounds. Studio v2 runs as a deployed web app — either self-hosted or at yourproject.sanity.studio — and its dependencies (React 17, Webpack 4, and a handful of older UI packages) are accumulating unpatched CVEs. Your content is stored in Sanity's cloud and is not directly at risk, but the Studio itself is an authenticated surface. If a dependency vulnerability allows script injection in your editor UI, an attacker who tricks an authenticated editor into visiting a crafted URL can act as that editor.

Beyond security: new Sanity features — AI Assist, document-level permissions, Presentations (live preview), the new media plugin, and Sanity TypeGen — are v3-only. Every month you stay on v2 is a month your editors are locked out of tooling that competitors' teams already have.

What actually breaks in the upgrade

The v2 to v3 jump is a ground-up rewrite on the Studio side. The underlying Content Lake API did not change — your GROQ queries, your dataset, and your documents are untouched. What changed is everything in the sanity.json / sanity.config.js layer and above.

Configuration format. v2 used a sanity.json manifest and a plugin-based folder structure. v3 uses a single sanity.config.ts file with a defineConfig() call. There is no automatic codemod that converts one to the other.

Plugin API. Every v2 plugin has a different import path and registration pattern in v3. Common ones — @sanity/vision, @sanity/desk-tool, sanity-plugin-media — all have v3 equivalents, but the package names changed and some configuration options moved. @sanity/desk-tool is now @sanity/structure and the builder API has minor but breaking differences (.child() chains work similarly, but S.document().views() changed).

Custom inputs. This is the most common source of pain. v2 custom inputs used React.forwardRef with a PatchEvent prop from part:@sanity/form-builder. v3 uses a completely different useFormValue / set / unset pattern from sanity. Every custom input your team wrote must be rewritten, not just re-imported.

Parts system gone. v2 had a parts system for overriding internal Studio components. v3 replaced this with explicit component overrides in defineConfig(). If you were using parts to customise the logo, navbar, or default document view, those customisations need to be ported to the new API.

Schema helpers. sanity/types now exports defineType, defineField, and defineArrayMember for TypeScript-safe schema authoring. Your existing schemas will still work without them, but mixing old object-literal schemas with new typed helpers in the same config can cause TypeScript errors you will need to triage.

Upgrade path by Studio size

The effort scales roughly with how much custom code sits on top of the default Studio.

Studio profileCustom inputsCustom structureEffort estimate
Default schemas, stock plugins0None2–4 hours
Several document types, 1–2 custom inputs1–3Basic S.list()1–2 days
Large schema set, multiple custom inputs, branded UI4+Complex nested structure3–5 days
Multi-workspace, document-level permissions, custom toolsManyMulti-dataset, custom tool tabs1–2 weeks

Those estimates assume someone who has done the migration before. If a developer is reading the migration guide for the first time while doing your project, add 50%.

The upgrade sequence that works

Do not try to upgrade in place on your main dataset. The sequence below has saved every project I have done from a messy rollback.

  1. Branch the Studio codebase. Create a studio-v3 branch. Keep v2 in main running for editors while you work.
  2. Bootstrap a clean v3 config. Run npm create sanity@latest in a scratch folder, copy the generated sanity.config.ts structure, then port your schemas one document type at a time.
  3. Port schemas first, validate with Vision. Get your schemas loading and query them with the Vision plugin before touching any UI customisation. Schemas are the lowest-risk layer.
  4. Migrate structure. Rebuild your deskTool() (now structureTool()) configuration. Test each list and nested pane.
  5. Rewrite custom inputs one at a time. Each custom input is a self-contained rewrite. Test on a non-production dataset.
  6. Replace parts overrides. Port logo, navbar, and document badge overrides to defineConfig() options.
  7. Run editors on staging for two weeks. Give your content team real access to the v3 Studio pointing at your production dataset (Sanity's API is the same — this is safe) before you cut over the main URL.
// sanity.config.ts — v3 shape (production-ready starting point)
import { defineConfig } from 'sanity'
import { structureTool } from 'sanity/structure'
import { visionTool } from '@sanity/vision'
import { schemaTypes } from './schemas'
 
export default defineConfig({
  name: 'default',
  title: 'My Studio',
  projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
  dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
  plugins: [
    structureTool(), // was deskTool() in v2
    visionTool(),
  ],
  schema: {
    types: schemaTypes,
  },
})

The v2 equivalent of that config was spread across sanity.json, deskStructure.js, and a parts array. One file is dramatically easier to maintain.

What a rewritten custom input looks like

// schemas/inputs/SlugPreviewInput.tsx — v3 custom input pattern
import { useFormValue, set, StringInputProps } from 'sanity'
 
export function SlugPreviewInput(props: StringInputProps) {
  const title = useFormValue(['title']) as string | undefined
 
  return (
    <div>
      {props.renderDefault(props)}
      {title && (
        <p style={{ fontSize: 12, color: '#888', marginTop: 4 }}>
          Auto-slug preview: {title.toLowerCase().replace(/\s+/g, '-')}
        </p>
      )}
    </div>
  )
}

The v2 version of the same input would have imported PatchEvent, patches, and a withDocument HOC — none of which exist in v3. This is a rewrite, not a refactor.

What to ask a developer before hiring

If you are scoping this work with a freelancer or agency, these questions separate someone who has done it from someone who is reading the docs at your expense.

  • Can you show me a v3 sanity.config.ts from a project you have shipped?
  • How do you handle the custom input rewrite — do you port one at a time or scaffold new ones?
  • Will you run our editors on the v3 Studio against our production dataset before cutting over?
  • What is your plan if a custom input breaks after cutover — do you keep v2 running on a backup URL?
  • Do you write TypeScript-safe schemas using defineType and defineField, or do you leave the schema as plain objects?

A developer who has done this before will have clear answers to all five. Someone reading the migration guide for the first time will hedge on the custom input question and will not mention a rollback plan.

The cost of another six months on v2

Every month on v2 is another month of unpatched dependencies, another month your editors cannot use AI Assist or Presentation, and another month of accumulated schema debt that makes the eventual migration harder. The upgrade is not trivial, but it is bounded work — typically one to two weeks for a real-world Studio with custom inputs and a structured desk.

This is work I do for teams on Sanity. If you have a v2 Studio that needs migrating — or you inherited one and are not sure what state it is in — I am happy to scope it from a schema audit.