A package.json you did not write is still a list of promises you now own.
The package.json at the root of your React Native and Expo project is a plain text list of what the app is made of, and you can read the whole thing in ten minutes without knowing React Native. It names every library that ships inside the app, every tool that exists only on your machine, the file the app starts from, and the shortcut commands somebody typed so you would not have to. Four lines in it carry real consequences and the rest are labels. This page is about telling those apart in a file you inherited, not about writing one from scratch.
Every field definition below is quoted from npm's own package.json reference or Expo's own package.json reference, both read on 17 October 2026. One of those two references says less than you would expect, and the gap is the most useful thing on this page.
Start hereThe short version
Two files pretending to be one with two owners and two rulebooks.
Most of the file is npm's. The dependency lists, the scripts block, the entry point, the name and version: those fields are defined by npm and mean the same thing in any JavaScript project in the world. Then there is a block called expo, which npm does not define, does not read and does not care about. Expo documents that one separately, and everything inside it is a switch on one of Expo's own checks.
Knowing which half you are looking at saves you the most common mistake with an inherited project, which is hand-editing a version number because the file looked editable. Worth saying early: none of this transfers to the other popular choice. A Flutter project keeps its dependency list in a pubspec.yaml with a different format and different rules, which is one of the quiet practical differences when people weigh Flutter against React Native.
What do the fields in a package.json actually mean?
Start with the two dependency lists, because they answer the question you actually have, which is what is inside the app you are now responsible for. npm defines dependencies as a simple object that maps a package name to a version range. That is the list that ships. devDependencies is the same shape for a different purpose, and npm is direct about the split: it asks you not to put test harnesses, transpilers or other development time tools in the dependencies object, and to map those in devDependencies instead. When you are triaging an inherited project this is the first useful cut you can make. Breakage in the second list stops your afternoon. Breakage in the first list is in the binary a customer downloads.
Then main, which is the single line in the file most likely to ruin a day. npm calls it a module ID that is the primary entry point to your program, and says that if main is not set, it defaults to index.js in the package root. In a modern Expo project it will not say index.js. Expo Router's installation page tells you to use expo-router/entry as the value, and that string is what wires your app directory up as the navigation tree. Point it at the wrong file and the app still launches. It simply shows nothing, with no error that explains why.
name and version matter less than they look. npm says the name and version together form an identifier that is assumed to be completely unique, which is a rule about publishing to the registry, and adds that if you do not plan to publish your package, the name and version fields are optional. You are shipping an app, not a library. Neither of those numbers is what a store displays. The field worth checking in that neighbourhood is private, because npm states that if you set private to true in your package.json, then npm will refuse to publish it. That is the one line standing between a tired evening and your entire app source on the public registry.
A few fields look like rules and are not. npm says the engines field is advisory only and will only produce warnings when your package is installed as a dependency, so it will not stop anyone building your app on the wrong version of Node. optionalDependencies is a list where, in npm's words, build failures do not cause installation to fail, which is a quiet way for something to be missing. And overrides, which npm notes are only considered in the root package.json file for a project, is where somebody forced a nested library to a version it did not ask for. Read every line under overrides as an unexplained incident report, because that is what it is.
npm, package.json reference for the npm CLI, read 17 October 2026
Line by line
Pick a line you can see in your file
Who put it there, what the documentation says it is for, and what happens if you change it.
main
Whose line it is: npm defines the field, Expo Router decides the value
What the documentation says: npm calls main a module ID that is the primary entry point to your program, and says that if main is not set it defaults to index.js in the package root. Expo Router's installation page tells you to use expo-router/entry as the value.
If you change it: High risk. This is the first file your app runs. If you point it somewhere else, the app starts, finds no routes, and shows you a blank screen with no error worth reading.
Field definitions from npm's and Expo's own references, read 17 October 2026. The risk notes are ours.
Which lines belong to Expo rather than to npm?
One block, and it is clearly labelled. Expo's own reference describes it as a reference to Expo-specific properties that can be used by adding an expo field in the package.json file. npm has no opinion about anything inside it. Today that reference documents three things: install, autolinking and doctor. Nothing else. If your file has an expo block with other keys in it, somebody added them by hand and you should find out who.
install.exclude is the one you are most likely to meet. Expo checks the versions of packages it knows about, and this array turns that check off for named libraries. The example in Expo's own documentation excludes two packages, expo-splash-screen and expo-updates, the second of which is the machinery behind over the air updates. That is a useful thing to know about an inherited project. A library sitting in install.exclude is a library somebody deliberately took outside Expo's version checking, which means it is pinned for a reason nobody wrote down.
doctor is the same idea applied to warnings. Expo says this object configures the behaviour of the expo-doctor command, and it holds two checks you can switch. One validates your packages against the React Native Directory, with keys for whether it is enabled, which packages to skip, and whether to list unknown ones. Expo states that by default this check is enabled and unknown packages are listed. The other warns when native android and ios directories sit next to an app config file whose fields are no longer being applied. Both default to on. Finding either of them switched off in a project you inherited is finding a warning somebody chose to stop seeing.
autolinking is the third, and in Expo's words it allows configuring module resolution behaviour by using the autolinking property in package.json. The documented example points nativeModulesDir at a local folder, which is how a project keeps its own native modules in the repository instead of pulling them from the registry. You will not often need to change it. You do need to notice it, because it means part of your app's native code is yours rather than somebody else's, and an upgrade will not bring it along for free.
What do the carets and tildes commit you to?
A range, not a version. That is the whole answer, and it is why the file is harder to read than it looks. npm's reference gives the operators in its own words: a tilde before a version is approximately equivalent to version, a caret is compatible with version, and a bare asterisk matches any version. So each dependency line is a statement about how much variation you agreed to accept, written by somebody who may not have thought about it. A caret is the default in most projects and the widest promise in the file, because it defers to the library author's own judgement about what counts as compatible.
Two entries in that column are not version ranges at all. npm says you may specify a tarball URL in place of a version range, and that dependencies can also be identified with a tarball or git URL, adding that a commit-ish after a hash will be used to clone exactly that commit. Both are legal and both change what your project depends on. A git dependency with no commit after the hash is a moving target. A tarball dependency means a file on somebody's web server is now part of your app, and the day that server goes away is the day you cannot build.
In an Expo project there is one line in this list you should not touch by hand, and it is react-native itself. Expo installs dependencies against a list of version combinations it knows work together, which is the entire reason it has its own install command rather than telling you to use npm directly. Expo's CLI reference documents a check flag that reports which installed packages need to be updated and a fix flag that automatically updates any invalid package versions, which is a far better first move than editing the number. That react-native line is also where Hermes and the new architecture get decided, which is why a one character edit there is never just a one character edit.
| What you see in the file | How npm describes it | What it commits you to |
|---|---|---|
| 1.2.3 | Must match version exactly | One version and no other. The clearest line in the file, and the one that goes stale quietly. |
| ~1.2.3 | Approximately equivalent to version | You have accepted small changes you will never read. Fine for a library you trust, awkward for one that touches native code. |
| ^1.2.3 | Compatible with version | The default almost everywhere, and the widest promise in the file. You are trusting the author's own judgement about what counts as compatible. |
| * | Matches any version | Nothing is promised at all. If you see this in an app you are responsible for, make it the first thing you ask about. |
| A git URL | Dependencies can also be identified with a tarball or git URL | The code comes from somebody's repository rather than the registry. npm adds that a commit-ish after a hash will be used to clone exactly that commit. No commit-ish means a moving target. |
| A tarball URL | You may specify a tarball URL in place of a version range | A file on a web server is now part of your app. If that server goes away, so does your ability to build. |
npm, package.json reference, dependency specifications, read 17 October 2026
What does Expo document about the scripts in your project?
Nothing, and that is worth knowing rather than guessing at. Expo documents what it reads out of your package.json, which is the expo block, and its reference does not mention the scripts field or the main field at all. We went looking in the four places it would be: the package.json reference, the create a project page, the start developing page and the Expo CLI reference. All four tell you to run the commands directly, written as npx expo start, rather than through a script in your file. Those pages were read on 17 October 2026.
So the scripts block in a project you inherited is a convenience somebody wrote, not a documented interface. npm defines the field as a dictionary containing script commands that are run at various times in the lifecycle of your package, and that is the end of the contract. Nothing checks that a script called start starts anything. npm does have its own conventions underneath, and one of them is worth knowing: if there is a server.js file in the root of your package, npm says it will default the start command to node server.js, and that prestart and poststart will still run in that case. A name can do something you did not ask for.
The practical consequence is small and real. Read the scripts block before you trust it, because that is the only documentation of it that exists. If your file has four lines there and they read like start, android, ios and web, run the one you need and watch what it prints. npm also notes that if you depend on modules that define executable scripts, those executables will be added to the PATH when a script runs, which is why a command that works inside a script fails when you type it straight into a terminal. That one costs people an hour the first time.
The scripts block is also where an upgrade shows up first. When the SDK moves, the commands people use move with it, and the honest place to check what changed is the Expo SDK release notes rather than a script somebody wrote two SDKs ago. A stale script does not fail loudly. It runs the old thing successfully and leaves you wondering why your change had no effect.
Where this lands if a generator wrote your manifest
Nothing in the file changes meaning because an agent typed it. The fields are npm's and Expo's, the promises are still promises, and you are still the person who answers for what ships. What does change is your starting assumption. In a project a stranger handed over, every line is an unexplained decision. In a generated project, most lines are the template and the interesting ones are the few that are not.
Newly is an AI app builder. You describe an app in plain English and it writes a real React Native and Expo project that you own. It uploads iOS builds to TestFlight, publishes to Google Play internal testing and builds a standalone Android APK. It keeps two-way GitHub sync, which matters here more than it sounds: the manifest lives in your repository, so you can read it in a browser, and an edit you make there comes back rather than being overwritten. It is $25 a month and there is no free tier.
The limit is worth stating plainly, because it is the same limit for every generator. A tool can write you a correct manifest. It cannot take responsibility for the range operators in it, and it cannot submit your app for you, because Apple requires the owner of the content to be the one who submits. That is why reading this file is not optional homework. Your first useful day with an inherited React Native project is the day you can open package.json, point at four lines, and say what each of them costs you.
Questions people ask about a package.json they inherited
It is the file at the root of the project that lists what the app is made of. npm's own reference describes the parts: dependencies is a simple object that maps a package name to a version range, devDependencies holds the tools you only need while working, scripts is a dictionary containing script commands that are run at various times in the lifecycle of your package, and main is a module ID that is the primary entry point to your program. In an Expo project there is also an expo object, which npm knows nothing about and Expo documents separately. Read in that order, the file tells you what ships, what does not, how people start the app, and which file it starts from.
Open the file, then open the four lines that matter
main, the dependency list, anything under overrides, and the expo block. Ten minutes on those four and an inherited project stops being a black box you are afraid to touch.
Start building