Repository: cli/cli
Pull request #8541 (2024-01-08 07:34:17)

Remove project JSON formatting objects
Remove near-duplicate types meant simply to format JSON for the `project` command group. Use a more efficient means through `ExportData`.

heaths: @samcoe this PR will be rebased on #7832 once that's merged and will implement . For now, I added a single commit to the current #7832 HEAD with regression tests to make sure the JSON is consistent but found a number of  commands didn't actually format the correct object.
heaths: Before I go too much further, do you feel these changes are on the right track?
samcoe: @heaths These changes are exactly what I had in mind from our discussions in #7832, definitely the right track.  
heaths: Rebased onto `trunk` now that #7832 is merged.
cliAutomation: Hi! Thanks for the pull request. Please ensure that this change is linked to an issue by mentioning an issue number in the description of the pull request. If this pull request would close the issue, please put the word 'Fixes' before the issue number somewhere in the pull request body. If this is a tiny change like fixing a typo, feel free to ignore this message.
samcoe: @heaths This looks great. Appreciate your help cleaning up this tech debt. My preference would be to delete `json.go` and to change the function invocations in `json_test.go` to use the `ExportData` method equivalents. Then the `json_test.go` file can be moved to something like `pkg/cmd/project/shared/queries/export_data_test.go`. How does that sound? Is that too much work? I don't think deleting the tests is necessary, they add some confidence that we haven't broken anything and add minimal time to the test suite. 
heaths: Nope, not too much work at all! I considered something like that initially as well but decided against it - at least within a single commit - to track any regressions. I know this will get squash-merged, but figured at least while it's under active review it might be good to "show my work". :wink:

Patch:
diff --git a/pkg/cmd/project/close/close.go b/pkg/cmd/project/close/close.go
index 73c61e888..d24278a23 100644
--- a/pkg/cmd/project/close/close.go
+++ b/pkg/cmd/project/close/close.go
@@ -6,7 +6,6 @@ import (
 
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
-	"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
@@ -104,7 +103,7 @@ func runClose(config closeConfig) error {
 	}
 
 	if config.opts.exporter != nil {
-		return printJSON(config, query.UpdateProjectV2.ProjectV2)
+		return config.opts.exporter.Write(config.io, query.UpdateProjectV2.ProjectV2)
 	}
 
 	return printResults(config, query.UpdateProjectV2.ProjectV2)
@@ -132,8 +131,3 @@ func printResults(config closeConfig, project queries.Project) error {
 	_, err := fmt.Fprintf(config.io.Out, "%s\n", project.URL)
 	return err
 }
-
-func printJSON(config closeConfig, project queries.Project) error {
-	projectJSON := format.JSONProject(project)
-	return config.opts.exporter.Write(config.io, projectJSON)
-}
diff --git a/pkg/cmd/project/copy/copy.go b/pkg/cmd/project/copy/copy.go
index 19fa3b0c1..63047f0a3 100644
--- a/pkg/cmd/project/copy/copy.go
+++ b/pkg/cmd/project/copy/copy.go
@@ -6,7 +6,6 @@ import (
 
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
-	"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
@@ -114,7 +113,7 @@ func runCopy(config copyConfig) error {
 	}
 
 	if config.opts.exporter != nil {
-		return printJSON(config, query.CopyProjectV2.ProjectV2)
+		return config.opts.exporter.Write(config.io, query.CopyProjectV2.ProjectV2)
 	}
 
 	return printResults(config, query.CopyProjectV2.ProjectV2)
@@ -142,8 +141,3 @@ func printResults(config copyConfig, project queries.Project) error {
 	_, err := fmt.Fprintf(config.io.Out, "%s\n", project.URL)
 	return err
 }
-
-func printJSON(config copyConfig, project queries.Project) error {
-	projectJSON := format.JSONProject(project)
-	return config.opts.exporter.Write(config.io, projectJSON)
-}
diff --git a/pkg/cmd/project/create/create.go b/pkg/cmd/project/create/create.go
index a914e0229..a261cb600 100644
--- a/pkg/cmd/project/create/create.go
+++ b/pkg/cmd/project/create/create.go
@@ -5,7 +5,6 @@ import (
 
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
-	"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
@@ -86,7 +85,7 @@ func runCreate(config createConfig) error {
 	}
 
 	if config.opts.exporter != nil {
-		return printJSON(config, query.CreateProjectV2.ProjectV2)
+		return config.opts.exporter.Write(config.io, query.CreateProjectV2.ProjectV2)
 	}
 
 	return printResults(config, query.CreateProjectV2.ProjectV2)
@@ -113,8 +112,3 @@ func printResults(config createConfig, project queries.Project) error {
 	_, err := fmt.Fprintf(config.io.Out, "%s\n", project.URL)
 	return err
 }
-
-func printJSON(config createConfig, project queries.Project) error {
-	projectJSON := format.JSONProject(project)
-	return config.opts.exporter.Write(config.io, projectJSON)
-}
diff --git a/pkg/cmd/project/delete/delete.go b/pkg/cmd/project/delete/delete.go
index 52c26ed4d..4848a52ae 100644
--- a/pkg/cmd/project/delete/delete.go
+++ b/pkg/cmd/project/delete/delete.go
@@ -6,7 +6,6 @@ import (
 
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
-	"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
@@ -97,7 +96,7 @@ func runDelete(config deleteConfig) error {
 	}
 
 	if config.opts.exporter != nil {
-		return printJSON(config, query.DeleteProject.Project)
+		return config.opts.exporter.Write(config.io, query.DeleteProject.Project)
 	}
 
 	return printResults(config, query.DeleteProject.Project)
@@ -124,8 +123,3 @@ func printResults(config deleteConfig, project queries.Project) error {
 	_, err := fmt.Fprintf(config.io.Out, "Deleted project %d\n", project.Number)
 	return err
 }
-
-func printJSON(config deleteConfig, project queries.Project) error {
-	projectJSON := format.JSONProject(project)
-	return config.opts.exporter.Write(config.io, projectJSON)
-}
diff --git a/pkg/cmd/project/edit/edit.go b/pkg/cmd/project/edit/edit.go
index 67f01e63d..f303ce357 100644
--- a/pkg/cmd/project/edit/edit.go
+++ b/pkg/cmd/project/edit/edit.go
@@ -6,7 +6,6 @@ import (
 
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
-	"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
@@ -111,7 +110,7 @@ func runEdit(config editConfig) error {
 	}
 
 	if config.opts.exporter != nil {
-		return printJSON(config, query.UpdateProjectV2.ProjectV2)
+		return config.opts.exporter.Write(config.io, query.UpdateProjectV2.ProjectV2)
 	}
 
 	return printResults(config, query.UpdateProjectV2.ProjectV2)
@@ -153,8 +152,3 @@ func printResults(config editConfig, project queries.Project) error {
 	_, err := fmt.Fprintf(config.io.Out, "%s\n", project.URL)
 	return err
 }
-
-func printJSON(config editConfig, project queries.Project) error {
-	projectJSON := format.JSONProject(project)
-	return config.opts.exporter.Write(config.io, projectJSON)
-}
diff --git a/pkg/cmd/project/field-create/field_create.go b/pkg/cmd/project/field-create/field_create.go
index c65c164de..56a305480 100644
--- a/pkg/cmd/project/field-create/field_create.go
+++ b/pkg/cmd/project/field-create/field_create.go
@@ -6,7 +6,6 @@ import (
 
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
-	"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
@@ -114,7 +113,7 @@ func runCreateField(config createFieldConfig) error {
 	}
 
 	if config.opts.exporter != nil {
-		return printJSON(config, query.CreateProjectV2Field.Field)
+		return config.opts.exporter.Write(config.io, query.CreateProjectV2Field.Field)
 	}
 
 	return printResults(config, query.CreateProjectV2Field.Field)
@@ -151,8 +150,3 @@ func printResults(config createFieldConfig, field queries.ProjectField) error {
 	_, err := fmt.Fprintf(config.io.Out, "Created field\n")
 	return err
 }
-
-func printJSON(config createFieldConfig, field queries.ProjectField) error {
-	projectFieldJSON := format.JSONProjectField(field)
-	return config.opts.exporter.Write(config.io, projectFieldJSON)
-}
diff --git a/pkg/cmd/project/field-delete/field_delete.go b/pkg/cmd/project/field-delete/field_delete.go
index a98d10b89..f8b97826b 100644
--- a/pkg/cmd/project/field-delete/field_delete.go
+++ b/pkg/cmd/project/field-delete/field_delete.go
@@ -4,7 +4,6 @@ import (
 	"fmt"
 
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
-	"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
@@ -71,7 +70,7 @@ func runDeleteField(config deleteFieldConfig) error {
 	}
 
 	if config.opts.exporter != nil {
-		return printJSON(config, query.DeleteProjectV2Field.Field)
+		return config.opts.exporter.Write(config.io, query.DeleteProjectV2Field.Field)
 	}
 
 	return printResults(config, query.DeleteProjectV2Field.Field)
@@ -93,8 +92,3 @@ func printResults(config deleteFieldConfig, field queries.ProjectField) error {
 	_, err := fmt.Fprintf(config.io.Out, "Deleted field\n")
 	return err
 }
-
-func printJSON(config deleteFieldConfig, field queries.ProjectField) error {
-	projectFieldJSON := format.JSONProjectField(field)
-	return config.opts.exporter.Write(config.io, projectFieldJSON)
-}
diff --git a/pkg/cmd/project/field-list/field_list.go b/pkg/cmd/project/field-list/field_list.go
index 88f261eeb..11964cb52 100644
--- a/pkg/cmd/project/field-list/field_list.go
+++ b/pkg/cmd/project/field-list/field_list.go
@@ -7,7 +7,6 @@ import (
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/internal/tableprinter"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
-	"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
@@ -95,7 +94,7 @@ func runList(config listConfig) error {
 	}
 
 	if config.opts.exporter != nil {
-		return printJSON(config, project)
+		return config.opts.exporter.Write(config.io, project.Fields)
 	}
 
 	return printResults(config, project.Fields.Nodes, owner.Login)
@@ -117,8 +116,3 @@ func printResults(config listConfig, fields []queries.ProjectField, login string
 
 	return tp.Render()
 }
-
-func printJSON(config listConfig, project *queries.Project) error {
-	projectsJSON := format.JSONProjectFields(project)
-	return config.opts.exporter.Write(config.io, projectsJSON)
-}
diff --git a/pkg/cmd/project/item-add/item_add.go b/pkg/cmd/project/item-add/item_add.go
index e3417952d..5e4eeff10 100644
--- a/pkg/cmd/project/item-add/item_add.go
+++ b/pkg/cmd/project/item-add/item_add.go
@@ -6,7 +6,6 @@ import (
 
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
-	"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
@@ -108,7 +107,7 @@ func runAddItem(config addItemConfig) error {
 	}
 
 	if config.opts.exporter != nil {
-		return printJSON(config, query.CreateProjectItem.ProjectV2Item)
+		return config.opts.exporter.Write(config.io, query.CreateProjectItem.ProjectV2Item)
 	}
 
 	return printResults(config, query.CreateProjectItem.ProjectV2Item)
@@ -132,8 +131,3 @@ func printResults(config addItemConfig, item queries.ProjectItem) error {
 	_, err := fmt.Fprintf(config.io.Out, "Added item\n")
 	return err
 }
-
-func printJSON(config addItemConfig, item queries.ProjectItem) error {
-	projectItemJSON := format.JSONProjectItem(item)
-	return config.opts.exporter.Write(config.io, projectItemJSON)
-}
diff --git a/pkg/cmd/project/item-archive/item_archive.go b/pkg/cmd/project/item-archive/item_archive.go
index 5eecd6d8a..18a820e86 100644
--- a/pkg/cmd/project/item-archive/item_archive.go
+++ b/pkg/cmd/project/item-archive/item_archive.go
@@ -6,7 +6,6 @@ import (
 
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
-	"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
@@ -110,7 +109,7 @@ func runArchiveItem(config archiveItemConfig) error {
 		}
 
 		if config.opts.exporter != nil {
-			return printJSON(config, query.UnarchiveProjectItem.ProjectV2Item)
+			return config.opts.exporter.Write(config.io, query.UnarchiveProjectItem.ProjectV2Item)
 		}
 
 		return printResults(config, query.UnarchiveProjectItem.ProjectV2Item)
@@ -122,7 +121,7 @@ func runArchiveItem(config archiveItemConfig) error {
 	}
 
 	if config.opts.exporter != nil {
-		return printJSON(config, query.ArchiveProjectItem.ProjectV2Item)
+		return config.opts.exporter.Write(config.io, query.ArchiveProjectItem.ProjectV2Item)
 	}
 
 	return printResults(config, query.ArchiveProjectItem.ProjectV2Item)
@@ -159,8 +158,3 @@ func printResults(config archiveItemConfig, item queries.ProjectItem) error {
 	_, err := fmt.Fprintf(config.io.Out, "Archived item\n")
 	return err
 }
-
-func printJSON(config archiveItemConfig, item queries.ProjectItem) error {
-	projectItemJSON := format.JSONProjectItem(item)
-	return config.opts.exporter.Write(config.io, projectItemJSON)
-}
diff --git a/pkg/cmd/project/item-create/item_create.go b/pkg/cmd/project/item-create/item_create.go
index 3b686916f..d1775172d 100644
--- a/pkg/cmd/project/item-create/item_create.go
+++ b/pkg/cmd/project/item-create/item_create.go
@@ -6,7 +6,6 @@ import (
 
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
-	"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
@@ -104,7 +103,7 @@ func runCreateItem(config createItemConfig) error {
 	}
 
 	if config.opts.exporter != nil {
-		return printJSON(config, query.CreateProjectDraftItem.ProjectV2Item)
+		return config.opts.exporter.Write(config.io, query.CreateProjectDraftItem.ProjectV2Item)
 	}
 
 	return printResults(config, query.CreateProjectDraftItem.ProjectV2Item)
@@ -128,8 +127,3 @@ func printResults(config createItemConfig, item queries.ProjectItem) error {
 	_, err := fmt.Fprintf(config.io.Out, "Created item\n")
 	return err
 }
-
-func printJSON(config createItemConfig, item queries.ProjectItem) error {
-	projectItemJSON := format.JSONProjectItem(item)
-	return config.opts.exporter.Write(config.io, projectItemJSON)
-}
diff --git a/pkg/cmd/project/item-edit/item_edit.go b/pkg/cmd/project/item-edit/item_edit.go
index b55d0b48f..63dcb3b39 100644
--- a/pkg/cmd/project/item-edit/item_edit.go
+++ b/pkg/cmd/project/item-edit/item_edit.go
@@ -7,7 +7,6 @@ import (
 
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
-	"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
@@ -219,11 +218,6 @@ func printDraftIssueResults(config editItemConfig, item queries.DraftIssue) erro
 	return err
 }
 
-func printDraftIssueJSON(config editItemConfig, item queries.DraftIssue) error {
-	projectDraftItemJSON := format.JSONProjectDraftIssue(item)
-	return config.opts.exporter.Write(config.io, projectDraftItemJSON)
-}
-
 func printItemResults(config editItemConfig, item *queries.ProjectItem) error {
 	if !config.io.IsStdoutTTY() {
 		return nil
@@ -232,11 +226,6 @@ func printItemResults(config editItemConfig, item *queries.ProjectItem) error {
 	return err
 }
 
-func printItemJSON(config editItemConfig, item *queries.ProjectItem) error {
-	projectItemJSON := format.JSONProjectItem(*item)
-	return config.opts.exporter.Write(config.io, projectItemJSON)
-}
-
 func clearItemFieldValue(config editItemConfig) error {
 	if err := fieldIdAndProjectIdPresence(config); err != nil {
 		return err
@@ -248,7 +237,7 @@ func clearItemFieldValue(config editItemConfig) error {
 	}
 
 	if config.opts.exporter != nil {
-		return printItemJSON(config, &query.Clear.Item)
+		return config.opts.exporter.Write(config.io, &query.Clear.Item)
 	}
 
 	return printItemResults(config, &query.Clear.Item)
@@ -267,7 +256,7 @@ func updateDraftIssue(config editItemConfig) error {
 	}
 
 	if config.opts.exporter != nil {
-		return printDraftIssueJSON(config, query.UpdateProjectV2DraftIssue.DraftIssue)
+		return config.opts.exporter.Write(config.io, query.UpdateProjectV2DraftIssue.DraftIssue)
 	}
 
 	return printDraftIssueResults(config, query.UpdateProjectV2DraftIssue.DraftIssue)
@@ -294,7 +283,7 @@ func updateItemValues(config editItemConfig) error {
 	}
 
 	if config.opts.exporter != nil {
-		return printItemJSON(config, &query.Update.Item)
+		return config.opts.exporter.Write(config.io, &query.Update.Item)
 	}
 
 	return printItemResults(config, &query.Update.Item)
diff --git a/pkg/cmd/project/item-list/item_list.go b/pkg/cmd/project/item-list/item_list.go
index 5f711f8e8..d62eb2716 100644
--- a/pkg/cmd/project/item-list/item_list.go
+++ b/pkg/cmd/project/item-list/item_list.go
@@ -7,7 +7,6 @@ import (
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/internal/tableprinter"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
-	"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
@@ -94,7 +93,7 @@ func runList(config listConfig) error {
 	}
 
 	if config.opts.exporter != nil {
-		return printJSON(config, project)
+		return config.opts.exporter.Write(config.io, project.DetailedItems())
 	}
 
 	return printResults(config, project.Items.Nodes, owner.Login)
@@ -122,8 +121,3 @@ func printResults(config listConfig, items []queries.ProjectItem, login string)
 
 	return tp.Render()
 }
-
-func printJSON(config listConfig, project *queries.Project) error {
-	projectDetailedItemsJSON := format.JSONProjectDetailedItems(project)
-	return config.opts.exporter.Write(config.io, projectDetailedItemsJSON)
-}
diff --git a/pkg/cmd/project/list/list.go b/pkg/cmd/project/list/list.go
index e345e3166..ba6e1d747 100644
--- a/pkg/cmd/project/list/list.go
+++ b/pkg/cmd/project/list/list.go
@@ -97,14 +97,14 @@ func runList(config listConfig) error {
 		return err
 	}
 
-	projects, totalCount, err := config.client.Projects(config.opts.owner, owner.Type, config.opts.limit, false)
+	projects, err := config.client.Projects(config.opts.owner, owner.Type, config.opts.limit, false)
 	if err != nil {
 		return err
 	}
 	projects = filterProjects(projects, config)
 
 	if config.opts.exporter != nil {
-		return printJSON(config, projects, totalCount)
+		return config.opts.exporter.Write(config.io, projects)
 	}
 
 	return printResults(config, projects, owner.Login)
@@ -138,26 +138,29 @@ func buildURL(config listConfig) (string, error) {
 	return url, nil
 }
 
-func filterProjects(nodes []queries.Project, config listConfig) []queries.Project {
-	projects := make([]queries.Project, 0, len(nodes))
-	for _, p := range nodes {
-		if !config.opts.closed && p.Closed {
+func filterProjects(nodes queries.Projects, config listConfig) queries.Projects {
+	filtered := queries.Projects{
+		Nodes:      make([]queries.Project, 0, len(nodes.Nodes)),
+		TotalCount: nodes.TotalCount,
+	}
+	for _, project := range nodes.Nodes {
+		if !config.opts.closed && project.Closed {
 			continue
 		}
-		projects = append(projects, p)
+		filtered.Nodes = append(filtered.Nodes, project)
 	}
-	return projects
+	return filtered
 }
 
-func printResults(config listConfig, projects []queries.Project, owner string) error {
-	if len(projects) == 0 {
+func printResults(config listConfig, projects queries.Projects, owner string) error {
+	if len(projects.Nodes) == 0 {
 		return cmdutil.NewNoResultsError(fmt.Sprintf("No projects found for %s", owner))
 	}
 
 	tp := tableprinter.New(config.io, tableprinter.WithHeader("Number", "Title", "State", "ID"))
 
 	cs := config.io.ColorScheme()
-	for _, p := range projects {
+	for _, p := range projects.Nodes {
 		tp.AddField(
 			strconv.Itoa(int(p.Number)),
 			tableprinter.WithTruncate(nil),
@@ -173,8 +176,3 @@ func printResults(config listConfig, projects []queries.Project, owner string) e
 
 	return tp.Render()
 }
-
-func printJSON(config listConfig, projects []queries.Project, totalCount int) error {
-	projectsJSON := format.JSONProjects(projects, totalCount)
-	return config.opts.exporter.Write(config.io, projectsJSON)
-}
diff --git a/pkg/cmd/project/mark-template/mark_template.go b/pkg/cmd/project/mark-template/mark_template.go
index 8931a2816..025fe2838 100644
--- a/pkg/cmd/project/mark-template/mark_template.go
+++ b/pkg/cmd/project/mark-template/mark_template.go
@@ -6,7 +6,6 @@ import (
 
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
-	"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
@@ -108,7 +107,7 @@ func runMarkTemplate(config markTemplateConfig) error {
 		}
 
 		if config.opts.exporter != nil {
-			return printJSON(config, *project)
+			return config.opts.exporter.Write(config.io, *project)
 		}
 
 		return printResults(config, query.TemplateProject.Project)
@@ -121,7 +120,7 @@ func runMarkTemplate(config markTemplateConfig) error {
 	}
 
 	if config.opts.exporter != nil {
-		return printJSON(config, query.TemplateProject.Project)
+		return config.opts.exporter.Write(config.io, query.TemplateProject.Project)
 	}
 
 	return printResults(config, query.TemplateProject.Project)
@@ -164,8 +163,3 @@ func printResults(config markTemplateConfig, project queries.Project) error {
 	_, err := fmt.Fprintf(config.io.Out, "Marked project %d as a template.\n", project.Number)
 	return err
 }
-
-func printJSON(config markTemplateConfig, project queries.Project) error {
-	projectJSON := format.JSONProject(project)
-	return config.opts.exporter.Write(config.io, projectJSON)
-}
diff --git a/pkg/cmd/project/shared/format/json.go b/pkg/cmd/project/shared/format/json.go
deleted file mode 100644
index 8f09f8ea4..000000000
--- a/pkg/cmd/project/shared/format/json.go
+++ /dev/null
@@ -1,386 +0,0 @@
-package format
-
-import (
-	"strings"
-
-	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
-)
-
-// JSONProject serializes a Project to JSON.
-func JSONProject(project queries.Project) ProjectJSON {
-	return ProjectJSON{
-		Number:           project.Number,
-		URL:              project.URL,
-		ShortDescription: project.ShortDescription,
-		Public:           project.Public,
-		Closed:           project.Closed,
-		// The Template field is commented out due to https://github.com/cli/cli/issues/8103.
-		// The Template field does not exist on GHES 3.8 and older, once GHES 3.8 gets
-		// deprecated on 2024-03-07 we can start populating this field again.
-		// Template: project.Template,
-		Title:  project.Title,
-		ID:     project.ID,
-		Readme: project.Readme,
-		Items: struct {
-			TotalCount int `json:"totalCount"`
-		}{
-			TotalCount: project.Items.TotalCount,
-		},
-		Fields: struct {
-			TotalCount int `json:"totalCount"`
-		}{
-			TotalCount: project.Fields.TotalCount,
-		},
-		Owner: struct {
-			Type  string `json:"type"`
-			Login string `json:"login"`
-		}{
-			Type:  project.OwnerType(),
-			Login: project.OwnerLogin(),
-		},
-	}
-}
-
-// JSONProjects serializes a slice of Projects to JSON.
-// JSON fields are `totalCount` and `projects`.
-func JSONProjects(projects []queries.Project, totalCount int) ProjectsJSON {
-	var result []ProjectJSON
-	for _, p := range projects {
-		result = append(result, ProjectJSON{
-			Number:           p.Number,
-			URL:              p.URL,
-			ShortDescription: p.ShortDescription,
-			Public:           p.Public,
-			Closed:           p.Closed,
-			// The Template field is commented out due to https://github.com/cli/cli/issues/8103.
-			// The Template field does not exist on GHES 3.8 and older, once GHES 3.8 gets
-			// deprecated on 2024-03-07 we can start populating this field again.
-			// Template: p.Template,
-			Title:  p.Title,
-			ID:     p.ID,
-			Readme: p.Readme,
-			Items: struct {
-				TotalCount int `json:"totalCount"`
-			}{
-				TotalCount: p.Items.TotalCount,
-			},
-			Fields: struct {
-				TotalCount int `json:"totalCount"`
-			}{
-				TotalCount: p.Fields.TotalCount,
-			},
-			Owner: struct {
-				Type  string `json:"type"`
-				Login string `json:"login"`
-			}{
-				Type:  p.OwnerType(),
-				Login: p.OwnerLogin(),
-			},
-		})
-	}
-
-	return ProjectsJSON{
-		Projects:   result,
-		TotalCount: totalCount,
-	}
-}
-
-type ProjectJSON struct {
-	Number           int32  `json:"number"`
-	URL              string `json:"url"`
-	ShortDescription string `json:"shortDescription"`
-	Public           bool   `json:"public"`
-	Closed           bool   `json:"closed"`
-	// The Template field is commented out due to https://github.com/cli/cli/issues/8103.
-	// The Template field does not exist on GHES 3.8 and older, once GHES 3.8 gets
-	// deprecated on 2024-03-07 we can start populating this field again.
-	// Template         bool   `json:"template"`
-	Title  string `json:"title"`
-	ID     string `json:"id"`
-	Readme string `json:"readme"`
-	Items  struct {
-		TotalCount int `json:"totalCount"`
-	} `graphql:"items(first: 100)" json:"items"`
-	Fields struct {
-		TotalCount int `json:"totalCount"`
-	} `graphql:"fields(first:100)" json:"fields"`
-	Owner struct {
-		Type  string `json:"type"`
-		Login string `json:"login"`
-	} `json:"owner"`
-}
-
-type ProjectsJSON struct {
-	Projects   []ProjectJSON `json:"projects"`
-	TotalCount int           `json:"totalCount"`
-}
-
-// JSONProjectField serializes a ProjectField to JSON.
-func JSONProjectField(field queries.ProjectField) ProjectFieldJSON {
-	val := ProjectFieldJSON{
-		ID:   field.ID(),
-		Name: field.Name(),
-		Type: field.Type(),
-	}
-	for _, o := range field.Options() {
-		val.Options = append(val.Options, SingleSelectOptionJSON{
-			Name: o.Name,
-			ID:   o.ID,
-		})
-	}
-
-	return val
-}
-
-// JSONProjectFields serializes a slice of ProjectFields to JSON.
-// JSON fields are `totalCount` and `fields`.
-func JSONProjectFields(project *queries.Project) ProjectFieldsJSON {
-	var result []ProjectFieldJSON
-	for _, f := range project.Fields.Nodes {
-		val := ProjectFieldJSON{
-			ID:   f.ID(),
-			Name: f.Name(),
-			Type: f.Type(),
-		}
-		for _, o := range f.Options() {
-			val.Options = append(val.Options, SingleSelectOptionJSON{
-				Name: o.Name,
-				ID:   o.ID,
-			})
-		}
-
-		result = append(result, val)
-	}
-
-	return ProjectFieldsJSON{
-		Fields:     result,
-		TotalCount: project.Fields.TotalCount,
-	}
-}
-
-type ProjectFieldJSON struct {
-	ID      string                   `json:"id"`
-	Name    string                   `json:"name"`
-	Type    string                   `json:"type"`
-	Options []SingleSelectOptionJSON `json:"options,omitempty"`
-}
-
-type ProjectFieldsJSON struct {
-	Fields     []ProjectFieldJSON `json:"fields"`
-	TotalCount int                `json:"totalCount"`
-}
-
-type SingleSelectOptionJSON struct {
-	ID   string `json:"id"`
-	Name string `json:"name"`
-}
-
-// JSONProjectItem serializes a ProjectItem to JSON.
-func JSONProjectItem(item queries.ProjectItem) ProjectItemJSON {
-	return ProjectItemJSON{
-		ID:    item.ID(),
-		Title: item.Title(),
-		Body:  item.Body(),
-		Type:  item.Type(),
-		URL:   item.URL(),
-	}
-}
-
-type ProjectItemJSON struct {
-	ID    string `json:"id"`
-	Title string `json:"title"`
-	Body  string `json:"body"`
-	Type  string `json:"type"`
-	URL   string `json:"url,omitempty"`
-}
-
-// JSONProjectDraftIssue serializes a DraftIssue to JSON.
-// This is needed because the field for
-// https://docs.github.com/en/graphql/reference/mutations#updateprojectv2draftissue
-// is a DraftIssue https://docs.github.com/en/graphql/reference/objects#draftissue
-// and not a ProjectV2Item https://docs.github.com/en/graphql/reference/objects#projectv2item
-func JSONProjectDraftIssue(item queries.DraftIssue) DraftIssueJSON {
-
-	return DraftIssueJSON{
-		ID:    item.ID,
-		Title: item.Title,
-		Body:  item.Body,
-		Type:  "DraftIssue",
-	}
-}
-
-type DraftIssueJSON struct {
-	ID    string `json:"id"`
-	Title string `json:"title"`
-	Body  string `json:"body"`
-	Type  string `json:"type"`
-}
-
-func projectItemContent(p queries.ProjectItem) any {
-	switch p.Content.TypeName {
-	case "DraftIssue":
-		return struct {
-			Type  string `json:"type"`
-			Body  string `json:"body"`
-			Title string `json:"title"`
-		}{
-			Type:  p.Type(),
-			Body:  p.Body(),
-			Title: p.Title(),
-		}
-	case "Issue":
-		return struct {
-			Type       string `json:"type"`
-			Body       string `json:"body"`
-			Title      string `json:"title"`
-			Number     int    `json:"number"`
-			Repository string `json:"repository"`
-			URL        string `json:"url"`
-		}{
-			Type:       p.Type(),
-			Body:       p.Body(),
-			Title:      p.Title(),
-			Number:     p.Number(),
-			Repository: p.Repo(),
-			URL:        p.URL(),
-		}
-	case "PullRequest":
-		return struct {
-			Type       string `json:"type"`
-			Body       string `json:"body"`
-			Title      string `json:"title"`
-			Number     int    `json:"number"`
-			Repository string `json:"repository"`
-			URL        string `json:"url"`
-		}{
-			Type:       p.Type(),
-			Body:       p.Body(),
-			Title:      p.Title(),
-			Number:     p.Number(),
-			Repository: p.Repo(),
-			URL:        p.URL(),
-		}
-	}
-
-	return nil
-}
-
-func projectFieldValueData(v queries.FieldValueNodes) any {
-	switch v.Type {
-	case "ProjectV2ItemFieldDateValue":
-		return v.ProjectV2ItemFieldDateValue.Date
-	case "ProjectV2ItemFieldIterationValue":
-		return struct {
-			Title     string `json:"title"`
-			StartDate string `json:"startDate"`
-			Duration  int    `json:"duration"`
-		}{
-			Title:     v.ProjectV2ItemFieldIterationValue.Title,
-			StartDate: v.ProjectV2ItemFieldIterationValue.StartDate,
-			Duration:  v.ProjectV2ItemFieldIterationValue.Duration,
-		}
-	case "ProjectV2ItemFieldNumberValue":
-		return v.ProjectV2ItemFieldNumberValue.Number
-	case "ProjectV2ItemFieldSingleSelectValue":
-		return v.ProjectV2ItemFieldSingleSelectValue.Name
-	case "ProjectV2ItemFieldTextValue":
-		return v.ProjectV2ItemFieldTextValue.Text
-	case "ProjectV2ItemFieldMilestoneValue":
-		return struct {
-			Title       string `json:"title"`
-			Description string `json:"description"`
-			DueOn       string `json:"dueOn"`
-		}{
-			Title:       v.ProjectV2ItemFieldMilestoneValue.Milestone.Title,
-			Description: v.ProjectV2ItemFieldMilestoneValue.Milestone.Description,
-			DueOn:       v.ProjectV2ItemFieldMilestoneValue.Milestone.DueOn,
-		}
-	case "ProjectV2ItemFieldLabelValue":
-		names := make([]string, 0)
-		for _, p := range v.ProjectV2ItemFieldLabelValue.Labels.Nodes {
-			names = append(names, p.Name)
-		}
-		return names
-	case "ProjectV2ItemFieldPullRequestValue":
-		urls := make([]string, 0)
-		for _, p := range v.ProjectV2ItemFieldPullRequestValue.PullRequests.Nodes {
-			urls = append(urls, p.Url)
-		}
-		return urls
-	case "ProjectV2ItemFieldRepositoryValue":
-		return v.ProjectV2ItemFieldRepositoryValue.Repository.Url
-	case "ProjectV2ItemFieldUserValue":
-		logins := make([]string, 0)
-		for _, p := range v.ProjectV2ItemFieldUserValue.Users.Nodes {
-			logins = append(logins, p.Login)
-		}
-		return logins
-	case "ProjectV2ItemFieldReviewerValue":
-		names := make([]string, 0)
-		for _, p := range v.ProjectV2ItemFieldReviewerValue.Reviewers.Nodes {
-			if p.Type == "Team" {
-				names = append(names, p.Team.Name)
-			} else if p.Type == "User" {
-				names = append(names, p.User.Login)
-			}
-		}
-		return names
-
-	}
-
-	return nil
-}
-
-// serialize creates a map from field to field values
-func serializeProjectWithItems(project *queries.Project) []map[string]any {
-	fields := make(map[string]string)
-
-	// make a map of fields by ID
-	for _, f := range project.Fields.Nodes {
-		fields[f.ID()] = camelCase(f.Name())
-	}
-	itemsSlice := make([]map[string]any, 0)
-
-	// for each value, look up the name by ID
-	// and set the value to the field value
-	for _, i := range project.Items.Nodes {
-		o := make(map[string]any)
-		o["id"] = i.Id
-		o["content"] = projectItemContent(i)
-		for _, v := range i.FieldValues.Nodes {
-			id := v.ID()
-			value := projectFieldValueData(v)
-
-			o[fields[id]] = value
-		}
-		itemsSlice = append(itemsSlice, o)
-	}
-	return itemsSlice
-}
-
-// JSONProjectWithItems returns a detailed JSON representation of project items.
-// JSON fields are `totalCount` and `items`.
-func JSONProjectDetailedItems(project *queries.Project) ProjectDetailedItems {
-	items := serializeProjectWithItems(project)
-	return ProjectDetailedItems{
-		Items:      items,
-		TotalCount: project.Items.TotalCount,
-	}
-}
-
-type ProjectDetailedItems struct {
-	Items      []map[string]any `json:"items"`
-	TotalCount int              `json:"totalCount"`
-}
-
-// camelCase converts a string to camelCase, which is useful for turning Go field names to JSON keys.
-func camelCase(s string) string {
-	if len(s) == 0 {
-		return ""
-	}
-
-	if len(s) == 1 {
-		return strings.ToLower(s)
-	}
-	return strings.ToLower(s[0:1]) + s[1:]
-}
diff --git a/pkg/cmd/project/shared/queries/queries.go b/pkg/cmd/project/shared/queries/queries.go
index c1c518848..354cf2cca 100644
--- a/pkg/cmd/project/shared/queries/queries.go
+++ b/pkg/cmd/project/shared/queries/queries.go
@@ -128,12 +128,8 @@ type Project struct {
 		TotalCount int
 		Nodes      []ProjectItem
 	} `graphql:"items(first: $firstItems, after: $afterItems)"`
-	Fields struct {
-		TotalCount int
-		Nodes      []ProjectField
-		PageInfo   PageInfo
-	} `graphql:"fields(first: $firstFields, after: $afterFields)"`
-	Owner struct {
+	Fields ProjectFields `graphql:"fields(first: $firstFields, after: $afterFields)"`
+	Owner  struct {
 		TypeName string `graphql:"__typename"`
 		User     struct {
 			Login string
@@ -144,6 +140,36 @@ type Project struct {
 	}
 }
 
+func (p Project) DetailedItems() map[string]interface{} {
+	return map[string]interface{}{
+		"items":      serializeProjectWithItems(&p),
+		"totalCount": p.Items.TotalCount,
+	}
+}
+
+func (p Project) ExportData(_ []string) map[string]interface{} {
+	return map[string]interface{}{
+		"number":           p.Number,
+		"url":              p.URL,
+		"shortDescription": p.ShortDescription,
+		"public":           p.Public,
+		"closed":           p.Closed,
+		"title":            p.Title,
+		"id":               p.ID,
+		"readme":           p.Readme,
+		"items": map[string]interface{}{
+			"totalCount": p.Items.TotalCount,
+		},
+		"fields": map[string]interface{}{
+			"totalCount": p.Fields.TotalCount,
+		},
+		"owner": map[string]interface{}{
+			"type":  p.OwnerType(),
+			"login": p.OwnerLogin(),
+		},
+	}
+}
+
 func (p Project) OwnerType() string {
 	return p.Owner.TypeName
 }
@@ -155,6 +181,22 @@ func (p Project) OwnerLogin() string {
 	return p.Owner.Organization.Login
 }
 
+type Projects struct {
+	Nodes      []Project
+	TotalCount int
+}
+
+func (p Projects) ExportData(_ []string) map[string]interface{} {
+	v := make([]map[string]interface{}, len(p.Nodes))
+	for i := range p.Nodes {
+		v[i] = p.Nodes[i].ExportData(nil)
+	}
+	return map[string]interface{}{
+		"projects":   v,
+		"totalCount": p.TotalCount,
+	}
+}
+
 // ProjectItem is a ProjectV2Item GraphQL object https://docs.github.com/en/graphql/reference/objects#projectv2item.
 type ProjectItem struct {
 	Content     ProjectItemContent
@@ -284,6 +326,19 @@ type DraftIssue struct {
 	Title string
 }
 
+func (i DraftIssue) ExportData(_ []string) map[string]interface{} {
+	v := map[string]interface{}{
+		"title": i.Title,
+		"body":  i.Body,
+		"type":  "DraftIssue",
+	}
+	// Emulate omitempty.
+	if i.ID != "" {
+		v["id"] = i.ID
+	}
+	return v
+}
+
 type PullRequest struct {
 	Body       string
 	Title      string
@@ -294,6 +349,17 @@ type PullRequest struct {
 	}
 }
 
+func (pr PullRequest) ExportData(_ []string) map[string]interface{} {
+	return map[string]interface{}{
+		"type":       "PullRequest",
+		"body":       pr.Body,
+		"title":      pr.Title,
+		"number":     pr.Number,
+		"repository": pr.Repository.NameWithOwner,
+		"url":        pr.URL,
+	}
+}
+
 type Issue struct {
 	Body       string
 	Title      string
@@ -304,6 +370,50 @@ type Issue struct {
 	}
 }
 
+func (i Issue) ExportData(_ []string) map[string]interface{} {
+	return map[string]interface{}{
+		"type":       "Issue",
+		"body":       i.Body,
+		"title":      i.Title,
+		"number":     i.Number,
+		"repository": i.Repository.NameWithOwner,
+		"url":        i.URL,
+	}
+}
+
+func (p ProjectItem) DetailedItem() exportable {
+	switch p.Type() {
+	case "DraftIssue":
+		return DraftIssue{
+			Body:  p.Body(),
+			Title: p.Title(),
+		}
+
+	case "Issue":
+		return Issue{
+			Body:   p.Body(),
+			Title:  p.Title(),
+			Number: p.Number(),
+			Repository: struct{ NameWithOwner string }{
+				NameWithOwner: p.Repo(),
+			},
+			URL: p.URL(),
+		}
+
+	case "PullRequest":
+		return PullRequest{
+			Body:   p.Body(),
+			Title:  p.Title(),
+			Number: p.Number(),
+			Repository: struct{ NameWithOwner string }{
+				NameWithOwner: p.Repo(),
+			},
+			URL: p.URL(),
+		}
+	}
+	return nil
+}
+
 // Type is the underlying type of the project item.
 func (p ProjectItem) Type() string {
 	return p.Content.TypeName
@@ -374,6 +484,20 @@ func (p ProjectItem) URL() string {
 	return ""
 }
 
+func (p ProjectItem) ExportData(_ []string) map[string]interface{} {
+	v := map[string]interface{}{
+		"id":    p.ID(),
+		"title": p.Title(),
+		"body":  p.Body(),
+		"type":  p.Type(),
+	}
+	// Emulate omitempty.
+	if url := p.URL(); url != "" {
+		v["url"] = url
+	}
+	return v
+}
+
 // ProjectItems returns the items of a project. If the OwnerType is VIEWER, no login is required.
 // If limit is 0, the default limit is used.
 func (c *Client) ProjectItems(o *Owner, number int32, limit int) (*Project, error) {
@@ -631,6 +755,13 @@ type SingleSelectFieldOptions struct {
 	Name string
 }
 
+func (f SingleSelectFieldOptions) ExportData(_ []string) map[string]interface{} {
+	return map[string]interface{}{
+		"id":   f.ID,
+		"name": f.Name,
+	}
+}
+
 func (p ProjectField) Options() []SingleSelectFieldOptions {
 	if p.TypeName == "ProjectV2SingleSelectField" {
 		var options []SingleSelectFieldOptions
@@ -645,6 +776,40 @@ func (p ProjectField) Options() []SingleSelectFieldOptions {
 	return nil
 }
 
+func (p ProjectField) ExportData(_ []string) map[string]interface{} {
+	v := map[string]interface{}{
+		"id":   p.ID(),
+		"name": p.Name(),
+		"type": p.Type(),
+	}
+	// Emulate omitempty
+	if opts := p.Options(); len(opts) != 0 {
+		options := make([]map[string]interface{}, len(opts))
+		for i, opt := range opts {
+			options[i] = opt.ExportData(nil)
+		}
+		v["options"] = options
+	}
+	return v
+}
+
+type ProjectFields struct {
+	TotalCount int
+	Nodes      []ProjectField
+	PageInfo   PageInfo
+}
+
+func (p ProjectFields) ExportData(_ []string) map[string]interface{} {
+	fields := make([]map[string]interface{}, len(p.Nodes))
+	for i := range p.Nodes {
+		fields[i] = p.Nodes[i].ExportData(nil)
+	}
+	return map[string]interface{}{
+		"fields":     fields,
+		"totalCount": p.TotalCount,
+	}
+}
+
 // ProjectFields returns a project with fields. If the OwnerType is VIEWER, no login is required.
 // If limit is 0, the default limit is used.
 func (c *Client) ProjectFields(o *Owner, number int32, limit int) (*Project, error) {
@@ -1084,17 +1249,17 @@ func (c *Client) NewProject(canPrompt bool, o *Owner, number int32, fields bool)
 		return nil, fmt.Errorf("project number is required when not running interactively")
 	}
 
-	projects, _, err := c.Projects(o.Login, o.Type, 0, fields)
+	projects, err := c.Projects(o.Login, o.Type, 0, fields)
 	if err != nil {
 		return nil, err
 	}
 
-	if len(projects) == 0 {
+	if len(projects.Nodes) == 0 {
 		return nil, fmt.Errorf("no projects found for %s", o.Login)
 	}
 
-	options := make([]string, 0, len(projects))
-	for _, p := range projects {
+	options := make([]string, 0, len(projects.Nodes))
+	for _, p := range projects.Nodes {
 		title := fmt.Sprintf("%s (#%d)", p.Title, p.Number)
 		options = append(options, title)
 	}
@@ -1104,16 +1269,17 @@ func (c *Client) NewProject(canPrompt bool, o *Owner, number int32, fields bool)
 		return nil, err
 	}
 
-	return &projects[answerIndex], nil
+	return &projects.Nodes[answerIndex], nil
 }
 
 // Projects returns all the projects for an Owner. If the OwnerType is VIEWER, no login is required.
 // If limit is 0, the default limit is used.
-func (c *Client) Projects(login string, t OwnerType, limit int, fields bool) ([]Project, int, error) {
-	projects := make([]Project, 0)
+func (c *Client) Projects(login string, t OwnerType, limit int, fields bool) (Projects, error) {
+	projects := Projects{
+		Nodes: make([]Project, 0),
+	}
 	cursor := (*githubv4.String)(nil)
 	hasNextPage := false
-	totalCount := 0
 
 	if limit == 0 {
 		limit = LimitDefault
@@ -1149,38 +1315,38 @@ func (c *Client) Projects(login string, t OwnerType, limit int, fields bool) ([]
 		if t == UserOwner {
 			var query userProjects
 			if err := c.doQuery("UserProjects", &query, variables); err != nil {
-				return projects, 0, err
+				return projects, err
 			}
-			projects = append(projects, query.Owner.Projects.Nodes...)
+			projects.Nodes = append(projects.Nodes, query.Owner.Projects.Nodes...)
 			hasNextPage = query.Owner.Projects.PageInfo.HasNextPage
 			cursor = &query.Owner.Projects.PageInfo.EndCursor
-			totalCount = query.Owner.Projects.TotalCount
+			projects.TotalCount = query.Owner.Projects.TotalCount
 		} else if t == OrgOwner {
 			var query orgProjects
 			if err := c.doQuery("OrgProjects", &query, variables); err != nil {
-				return projects, 0, err
+				return projects, err
 			}
-			projects = append(projects, query.Owner.Projects.Nodes...)
+			projects.Nodes = append(projects.Nodes, query.Owner.Projects.Nodes...)
 			hasNextPage = query.Owner.Projects.PageInfo.HasNextPage
 			cursor = &query.Owner.Projects.PageInfo.EndCursor
-			totalCount = query.Owner.Projects.TotalCount
+			projects.TotalCount = query.Owner.Projects.TotalCount
 		} else if t == ViewerOwner {
 			var query viewerProjects
 			if err := c.doQuery("ViewerProjects", &query, variables); err != nil {
-				return projects, 0, err
+				return projects, err
 			}
-			projects = append(projects, query.Owner.Projects.Nodes...)
+			projects.Nodes = append(projects.Nodes, query.Owner.Projects.Nodes...)
 			hasNextPage = query.Owner.Projects.PageInfo.HasNextPage
 			cursor = &query.Owner.Projects.PageInfo.EndCursor
-			totalCount = query.Owner.Projects.TotalCount
+			projects.TotalCount = query.Owner.Projects.TotalCount
 		}
 
-		if !hasNextPage || len(projects) >= limit {
-			return projects, totalCount, nil
+		if !hasNextPage || len(projects.Nodes) >= limit {
+			return projects, nil
 		}
 
-		if len(projects)+LimitMax > limit {
-			first := limit - len(projects)
+		if len(projects.Nodes)+LimitMax > limit {
+			first := limit - len(projects.Nodes)
 			variables["first"] = githubv4.Int(first)
 		}
 		variables["after"] = cursor
@@ -1223,3 +1389,107 @@ func requiredScopesFromServerMessage(msg string) []string {
 	}
 	return scopes
 }
+
+func projectFieldValueData(v FieldValueNodes) interface{} {
+	switch v.Type {
+	case "ProjectV2ItemFieldDateValue":
+		return v.ProjectV2ItemFieldDateValue.Date
+	case "ProjectV2ItemFieldIterationValue":
+		return map[string]interface{}{
+			"title":     v.ProjectV2ItemFieldIterationValue.Title,
+			"startDate": v.ProjectV2ItemFieldIterationValue.StartDate,
+			"duration":  v.ProjectV2ItemFieldIterationValue.Duration,
+		}
+	case "ProjectV2ItemFieldNumberValue":
+		return v.ProjectV2ItemFieldNumberValue.Number
+	case "ProjectV2ItemFieldSingleSelectValue":
+		return v.ProjectV2ItemFieldSingleSelectValue.Name
+	case "ProjectV2ItemFieldTextValue":
+		return v.ProjectV2ItemFieldTextValue.Text
+	case "ProjectV2ItemFieldMilestoneValue":
+		return map[string]interface{}{
+			"title":       v.ProjectV2ItemFieldMilestoneValue.Milestone.Title,
+			"description": v.ProjectV2ItemFieldMilestoneValue.Milestone.Description,
+			"dueOn":       v.ProjectV2ItemFieldMilestoneValue.Milestone.DueOn,
+		}
+	case "ProjectV2ItemFieldLabelValue":
+		names := make([]string, 0)
+		for _, p := range v.ProjectV2ItemFieldLabelValue.Labels.Nodes {
+			names = append(names, p.Name)
+		}
+		return names
+	case "ProjectV2ItemFieldPullRequestValue":
+		urls := make([]string, 0)
+		for _, p := range v.ProjectV2ItemFieldPullRequestValue.PullRequests.Nodes {
+			urls = append(urls, p.Url)
+		}
+		return urls
+	case "ProjectV2ItemFieldRepositoryValue":
+		return v.ProjectV2ItemFieldRepositoryValue.Repository.Url
+	case "ProjectV2ItemFieldUserValue":
+		logins := make([]string, 0)
+		for _, p := range v.ProjectV2ItemFieldUserValue.Users.Nodes {
+			logins = append(logins, p.Login)
+		}
+		return logins
+	case "ProjectV2ItemFieldReviewerValue":
+		names := make([]string, 0)
+		for _, p := range v.ProjectV2ItemFieldReviewerValue.Reviewers.Nodes {
+			if p.Type == "Team" {
+				names = append(names, p.Team.Name)
+			} else if p.Type == "User" {
+				names = append(names, p.User.Login)
+			}
+		}
+		return names
+
+	}
+
+	return nil
+}
+
+// serialize creates a map from field to field values
+func serializeProjectWithItems(project *Project) []map[string]interface{} {
+	fields := make(map[string]string)
+
+	// make a map of fields by ID
+	for _, f := range project.Fields.Nodes {
+		fields[f.ID()] = camelCase(f.Name())
+	}
+	itemsSlice := make([]map[string]interface{}, 0)
+
+	// for each value, look up the name by ID
+	// and set the value to the field value
+	for _, i := range project.Items.Nodes {
+		o := make(map[string]interface{})
+		o["id"] = i.Id
+		if projectItem := i.DetailedItem(); projectItem != nil {
+			o["content"] = projectItem.ExportData(nil)
+		} else {
+			o["content"] = nil
+		}
+		for _, v := range i.FieldValues.Nodes {
+			id := v.ID()
+			value := projectFieldValueData(v)
+
+			o[fields[id]] = value
+		}
+		itemsSlice = append(itemsSlice, o)
+	}
+	return itemsSlice
+}
+
+// camelCase converts a string to camelCase, which is useful for turning Go field names to JSON keys.
+func camelCase(s string) string {
+	if len(s) == 0 {
+		return ""
+	}
+	if len(s) == 1 {
+		return strings.ToLower(s)
+	}
+	return strings.ToLower(s[0:1]) + s[1:]
+}
+
+type exportable interface {
+	ExportData([]string) map[string]interface{}
+}
diff --git a/pkg/cmd/project/view/view.go b/pkg/cmd/project/view/view.go
index 8340149b5..ce54e1f09 100644
--- a/pkg/cmd/project/view/view.go
+++ b/pkg/cmd/project/view/view.go
@@ -7,7 +7,6 @@ import (
 
 	"github.com/MakeNowJust/heredoc"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/client"
-	"github.com/cli/cli/v2/pkg/cmd/project/shared/format"
 	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
@@ -107,7 +106,7 @@ func runView(config viewConfig) error {
 	}
 
 	if config.opts.exporter != nil {
-		return printJSON(config, *project)
+		return config.opts.exporter.Write(config.io, *project)
 	}
 
 	return printResults(config, project)
@@ -191,8 +190,3 @@ func printResults(config viewConfig, project *queries.Project) error {
 	_, err = fmt.Fprint(config.io.Out, out)
 	return err
 }
-
-func printJSON(config viewConfig, project queries.Project) error {
-	projectJSON := format.JSONProject(project)
-	return config.opts.exporter.Write(config.io, projectJSON)
-}


Test patch:
diff --git a/pkg/cmd/project/item-edit/item_edit_test.go b/pkg/cmd/project/item-edit/item_edit_test.go
index 9734c2b0d..fb9097d19 100644
--- a/pkg/cmd/project/item-edit/item_edit_test.go
+++ b/pkg/cmd/project/item-edit/item_edit_test.go
@@ -546,6 +546,7 @@ func TestRunItemEdit_JSON(t *testing.T) {
 			"data": map[string]interface{}{
 				"updateProjectV2DraftIssue": map[string]interface{}{
 					"draftIssue": map[string]interface{}{
+						"id":    "DI_item_id",
 						"title": "a title",
 						"body":  "a new body",
 					},
@@ -571,6 +572,6 @@ func TestRunItemEdit_JSON(t *testing.T) {
 	assert.NoError(t, err)
 	assert.JSONEq(
 		t,
-		`{"id":"","title":"a title","body":"a new body","type":"DraftIssue"}`,
+		`{"id":"DI_item_id","title":"a title","body":"a new body","type":"DraftIssue"}`,
 		stdout.String())
 }
diff --git a/pkg/cmd/project/shared/format/json_test.go b/pkg/cmd/project/shared/queries/export_data_test.go
similarity index 61%
rename from pkg/cmd/project/shared/format/json_test.go
rename to pkg/cmd/project/shared/queries/export_data_test.go
index 374fa8751..976839d43 100644
--- a/pkg/cmd/project/shared/format/json_test.go
+++ b/pkg/cmd/project/shared/queries/export_data_test.go
@@ -1,16 +1,15 @@
-package format
+package queries
 
 import (
 	"encoding/json"
 	"testing"
 
-	"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
-
 	"github.com/stretchr/testify/assert"
 )
 
+// Regression test from before ExportData was implemented.
 func TestJSONProject_User(t *testing.T) {
-	project := queries.Project{
+	project := Project{
 		ID:               "123",
 		Number:           2,
 		URL:              "a url",
@@ -23,14 +22,15 @@ func TestJSONProject_User(t *testing.T) {
 	project.Fields.TotalCount = 2
 	project.Owner.TypeName = "User"
 	project.Owner.User.Login = "monalisa"
-	b, err := json.Marshal(JSONProject(project))
+	b, err := json.Marshal(project.ExportData(nil))
 	assert.NoError(t, err)
 
-	assert.Equal(t, `{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"User","login":"monalisa"}}`, string(b))
+	assert.JSONEq(t, `{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"User","login":"monalisa"}}`, string(b))
 }
 
+// Regression test from before ExportData was implemented.
 func TestJSONProject_Org(t *testing.T) {
-	project := queries.Project{
+	project := Project{
 		ID:               "123",
 		Number:           2,
 		URL:              "a url",
@@ -43,14 +43,15 @@ func TestJSONProject_Org(t *testing.T) {
 	project.Fields.TotalCount = 2
 	project.Owner.TypeName = "Organization"
 	project.Owner.Organization.Login = "github"
-	b, err := json.Marshal(JSONProject(project))
+	b, err := json.Marshal(project.ExportData(nil))
 	assert.NoError(t, err)
 
-	assert.Equal(t, `{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"Organization","login":"github"}}`, string(b))
+	assert.JSONEq(t, `{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"Organization","login":"github"}}`, string(b))
 }
 
+// Regression test from before ExportData was implemented.
 func TestJSONProjects(t *testing.T) {
-	userProject := queries.Project{
+	userProject := Project{
 		ID:               "123",
 		Number:           2,
 		URL:              "a url",
@@ -64,7 +65,7 @@ func TestJSONProjects(t *testing.T) {
 	userProject.Owner.TypeName = "User"
 	userProject.Owner.User.Login = "monalisa"
 
-	orgProject := queries.Project{
+	orgProject := Project{
 		ID:               "123",
 		Number:           2,
 		URL:              "a url",
@@ -77,34 +78,38 @@ func TestJSONProjects(t *testing.T) {
 	orgProject.Fields.TotalCount = 2
 	orgProject.Owner.TypeName = "Organization"
 	orgProject.Owner.Organization.Login = "github"
-	projectsJSON := JSONProjects([]queries.Project{userProject, orgProject}, 2)
-	b, err := json.Marshal(projectsJSON)
+
+	projects := Projects{
+		Nodes:      []Project{userProject, orgProject},
+		TotalCount: 2,
+	}
+	b, err := json.Marshal(projects.ExportData(nil))
 	assert.NoError(t, err)
 
-	assert.Equal(
+	assert.JSONEq(
 		t,
 		`{"projects":[{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"User","login":"monalisa"}},{"number":2,"url":"a url","shortDescription":"short description","public":true,"closed":false,"title":"","id":"123","readme":"readme","items":{"totalCount":1},"fields":{"totalCount":2},"owner":{"type":"Organization","login":"github"}}],"totalCount":2}`,
 		string(b))
 }
 
 func TestJSONProjectField_FieldType(t *testing.T) {
-	field := queries.ProjectField{}
+	field := ProjectField{}
 	field.TypeName = "ProjectV2Field"
 	field.Field.ID = "123"
 	field.Field.Name = "name"
 
-	b, err := json.Marshal(JSONProjectField(field))
+	b, err := json.Marshal(field.ExportData(nil))
 	assert.NoError(t, err)
 
 	assert.Equal(t, `{"id":"123","name":"name","type":"ProjectV2Field"}`, string(b))
 }
 
 func TestJSONProjectField_SingleSelectType(t *testing.T) {
-	field := queries.ProjectField{}
+	field := ProjectField{}
 	field.TypeName = "ProjectV2SingleSelectField"
 	field.SingleSelectField.ID = "123"
 	field.SingleSelectField.Name = "name"
-	field.SingleSelectField.Options = []queries.SingleSelectFieldOptions{
+	field.SingleSelectField.Options = []SingleSelectFieldOptions{
 		{
 			ID:   "123",
 			Name: "name",
@@ -115,35 +120,35 @@ func TestJSONProjectField_SingleSelectType(t *testing.T) {
 		},
 	}
 
-	b, err := json.Marshal(JSONProjectField(field))
+	b, err := json.Marshal(field.ExportData(nil))
 	assert.NoError(t, err)
 
-	assert.Equal(t, `{"id":"123","name":"name","type":"ProjectV2SingleSelectField","options":[{"id":"123","name":"name"},{"id":"456","name":"name2"}]}`, string(b))
+	assert.JSONEq(t, `{"id":"123","name":"name","type":"ProjectV2SingleSelectField","options":[{"id":"123","name":"name"},{"id":"456","name":"name2"}]}`, string(b))
 }
 
 func TestJSONProjectField_ProjectV2IterationField(t *testing.T) {
-	field := queries.ProjectField{}
+	field := ProjectField{}
 	field.TypeName = "ProjectV2IterationField"
 	field.IterationField.ID = "123"
 	field.IterationField.Name = "name"
 
-	b, err := json.Marshal(JSONProjectField(field))
+	b, err := json.Marshal(field.ExportData(nil))
 	assert.NoError(t, err)
 
 	assert.Equal(t, `{"id":"123","name":"name","type":"ProjectV2IterationField"}`, string(b))
 }
 
 func TestJSONProjectFields(t *testing.T) {
-	field := queries.ProjectField{}
+	field := ProjectField{}
 	field.TypeName = "ProjectV2Field"
 	field.Field.ID = "123"
 	field.Field.Name = "name"
 
-	field2 := queries.ProjectField{}
+	field2 := ProjectField{}
 	field2.TypeName = "ProjectV2SingleSelectField"
 	field2.SingleSelectField.ID = "123"
 	field2.SingleSelectField.Name = "name"
-	field2.SingleSelectField.Options = []queries.SingleSelectFieldOptions{
+	field2.SingleSelectField.Options = []SingleSelectFieldOptions{
 		{
 			ID:   "123",
 			Name: "name",
@@ -154,73 +159,72 @@ func TestJSONProjectFields(t *testing.T) {
 		},
 	}
 
-	p := &queries.Project{
+	p := &Project{
 		Fields: struct {
 			TotalCount int
-			Nodes      []queries.ProjectField
-			PageInfo   queries.PageInfo
+			Nodes      []ProjectField
+			PageInfo   PageInfo
 		}{
-			Nodes:      []queries.ProjectField{field, field2},
+			Nodes:      []ProjectField{field, field2},
 			TotalCount: 5,
 		},
 	}
-	projectFieldsJSON := JSONProjectFields(p)
-	b, err := json.Marshal(projectFieldsJSON)
+	b, err := json.Marshal(p.Fields.ExportData(nil))
 	assert.NoError(t, err)
 
-	assert.Equal(t, `{"fields":[{"id":"123","name":"name","type":"ProjectV2Field"},{"id":"123","name":"name","type":"ProjectV2SingleSelectField","options":[{"id":"123","name":"name"},{"id":"456","name":"name2"}]}],"totalCount":5}`, string(b))
+	assert.JSONEq(t, `{"fields":[{"id":"123","name":"name","type":"ProjectV2Field"},{"id":"123","name":"name","type":"ProjectV2SingleSelectField","options":[{"id":"123","name":"name"},{"id":"456","name":"name2"}]}],"totalCount":5}`, string(b))
 }
 
 func TestJSONProjectItem_DraftIssue(t *testing.T) {
-	item := queries.ProjectItem{}
+	item := ProjectItem{}
 	item.Content.TypeName = "DraftIssue"
 	item.Id = "123"
 	item.Content.DraftIssue.Title = "title"
 	item.Content.DraftIssue.Body = "a body"
 
-	b, err := json.Marshal(JSONProjectItem(item))
+	b, err := json.Marshal(item.ExportData(nil))
 	assert.NoError(t, err)
 
-	assert.Equal(t, `{"id":"123","title":"title","body":"a body","type":"DraftIssue"}`, string(b))
+	assert.JSONEq(t, `{"id":"123","title":"title","body":"a body","type":"DraftIssue"}`, string(b))
 }
 
 func TestJSONProjectItem_Issue(t *testing.T) {
-	item := queries.ProjectItem{}
+	item := ProjectItem{}
 	item.Content.TypeName = "Issue"
 	item.Id = "123"
 	item.Content.Issue.Title = "title"
 	item.Content.Issue.Body = "a body"
 	item.Content.Issue.URL = "a-url"
 
-	b, err := json.Marshal(JSONProjectItem(item))
+	b, err := json.Marshal(item.ExportData(nil))
 	assert.NoError(t, err)
 
-	assert.Equal(t, `{"id":"123","title":"title","body":"a body","type":"Issue","url":"a-url"}`, string(b))
+	assert.JSONEq(t, `{"id":"123","title":"title","body":"a body","type":"Issue","url":"a-url"}`, string(b))
 }
 
 func TestJSONProjectItem_PullRequest(t *testing.T) {
-	item := queries.ProjectItem{}
+	item := ProjectItem{}
 	item.Content.TypeName = "PullRequest"
 	item.Id = "123"
 	item.Content.PullRequest.Title = "title"
 	item.Content.PullRequest.Body = "a body"
 	item.Content.PullRequest.URL = "a-url"
 
-	b, err := json.Marshal(JSONProjectItem(item))
+	b, err := json.Marshal(item.ExportData(nil))
 	assert.NoError(t, err)
 
-	assert.Equal(t, `{"id":"123","title":"title","body":"a body","type":"PullRequest","url":"a-url"}`, string(b))
+	assert.JSONEq(t, `{"id":"123","title":"title","body":"a body","type":"PullRequest","url":"a-url"}`, string(b))
 }
 
 func TestJSONProjectDetailedItems(t *testing.T) {
-	p := &queries.Project{}
+	p := &Project{}
 	p.Items.TotalCount = 5
-	p.Items.Nodes = []queries.ProjectItem{
+	p.Items.Nodes = []ProjectItem{
 		{
 			Id: "issueId",
-			Content: queries.ProjectItemContent{
+			Content: ProjectItemContent{
 				TypeName: "Issue",
-				Issue: queries.Issue{
+				Issue: Issue{
 					Title:  "Issue title",
 					Body:   "a body",
 					Number: 1,
@@ -235,9 +239,9 @@ func TestJSONProjectDetailedItems(t *testing.T) {
 		},
 		{
 			Id: "pullRequestId",
-			Content: queries.ProjectItemContent{
+			Content: ProjectItemContent{
 				TypeName: "PullRequest",
-				PullRequest: queries.PullRequest{
+				PullRequest: PullRequest{
 					Title:  "Pull Request title",
 					Body:   "a body",
 					Number: 2,
@@ -252,9 +256,9 @@ func TestJSONProjectDetailedItems(t *testing.T) {
 		},
 		{
 			Id: "draftIssueId",
-			Content: queries.ProjectItemContent{
+			Content: ProjectItemContent{
 				TypeName: "DraftIssue",
-				DraftIssue: queries.DraftIssue{
+				DraftIssue: DraftIssue{
 					Title: "Pull Request title",
 					Body:  "a body",
 				},
@@ -262,56 +266,56 @@ func TestJSONProjectDetailedItems(t *testing.T) {
 		},
 	}
 
-	out, err := json.Marshal(JSONProjectDetailedItems(p))
+	out, err := json.Marshal(p.DetailedItems())
 	assert.NoError(t, err)
-	assert.Equal(
+	assert.JSONEq(
 		t,
 		`{"items":[{"content":{"type":"Issue","body":"a body","title":"Issue title","number":1,"repository":"cli/go-gh","url":"issue-url"},"id":"issueId"},{"content":{"type":"PullRequest","body":"a body","title":"Pull Request title","number":2,"repository":"cli/go-gh","url":"pr-url"},"id":"pullRequestId"},{"content":{"type":"DraftIssue","body":"a body","title":"Pull Request title"},"id":"draftIssueId"}],"totalCount":5}`,
 		string(out))
 }
 
 func TestJSONProjectDraftIssue(t *testing.T) {
-	item := queries.DraftIssue{}
+	item := DraftIssue{}
 	item.ID = "123"
 	item.Title = "title"
 	item.Body = "a body"
 
-	b, err := json.Marshal(JSONProjectDraftIssue(item))
+	b, err := json.Marshal(item.ExportData(nil))
 	assert.NoError(t, err)
 
-	assert.Equal(t, `{"id":"123","title":"title","body":"a body","type":"DraftIssue"}`, string(b))
+	assert.JSONEq(t, `{"id":"123","title":"title","body":"a body","type":"DraftIssue"}`, string(b))
 }
 
 func TestJSONProjectItem_DraftIssue_ProjectV2ItemFieldIterationValue(t *testing.T) {
-	iterationField := queries.ProjectField{TypeName: "ProjectV2IterationField"}
+	iterationField := ProjectField{TypeName: "ProjectV2IterationField"}
 	iterationField.IterationField.ID = "sprint"
 	iterationField.IterationField.Name = "Sprint"
 
-	iterationFieldValue := queries.FieldValueNodes{Type: "ProjectV2ItemFieldIterationValue"}
+	iterationFieldValue := FieldValueNodes{Type: "ProjectV2ItemFieldIterationValue"}
 	iterationFieldValue.ProjectV2ItemFieldIterationValue.Title = "Iteration Title"
 	iterationFieldValue.ProjectV2ItemFieldIterationValue.Field = iterationField
 
-	draftIssue := queries.ProjectItem{
+	draftIssue := ProjectItem{
 		Id: "draftIssueId",
-		Content: queries.ProjectItemContent{
+		Content: ProjectItemContent{
 			TypeName: "DraftIssue",
-			DraftIssue: queries.DraftIssue{
+			DraftIssue: DraftIssue{
 				Title: "Pull Request title",
 				Body:  "a body",
 			},
 		},
 	}
-	draftIssue.FieldValues.Nodes = []queries.FieldValueNodes{
+	draftIssue.FieldValues.Nodes = []FieldValueNodes{
 		iterationFieldValue,
 	}
-	p := &queries.Project{}
-	p.Fields.Nodes = []queries.ProjectField{iterationField}
+	p := &Project{}
+	p.Fields.Nodes = []ProjectField{iterationField}
 	p.Items.TotalCount = 5
-	p.Items.Nodes = []queries.ProjectItem{
+	p.Items.Nodes = []ProjectItem{
 		draftIssue,
 	}
 
-	out, err := json.Marshal(JSONProjectDetailedItems(p))
+	out, err := json.Marshal(p.DetailedItems())
 	assert.NoError(t, err)
 	assert.JSONEq(
 		t,
@@ -321,35 +325,35 @@ func TestJSONProjectItem_DraftIssue_ProjectV2ItemFieldIterationValue(t *testing.
 }
 
 func TestJSONProjectItem_DraftIssue_ProjectV2ItemFieldMilestoneValue(t *testing.T) {
-	milestoneField := queries.ProjectField{TypeName: "ProjectV2IterationField"}
+	milestoneField := ProjectField{TypeName: "ProjectV2IterationField"}
 	milestoneField.IterationField.ID = "milestone"
 	milestoneField.IterationField.Name = "Milestone"
 
-	milestoneFieldValue := queries.FieldValueNodes{Type: "ProjectV2ItemFieldMilestoneValue"}
+	milestoneFieldValue := FieldValueNodes{Type: "ProjectV2ItemFieldMilestoneValue"}
 	milestoneFieldValue.ProjectV2ItemFieldMilestoneValue.Milestone.Title = "Milestone Title"
 	milestoneFieldValue.ProjectV2ItemFieldMilestoneValue.Field = milestoneField
 
-	draftIssue := queries.ProjectItem{
+	draftIssue := ProjectItem{
 		Id: "draftIssueId",
-		Content: queries.ProjectItemContent{
+		Content: ProjectItemContent{
 			TypeName: "DraftIssue",
-			DraftIssue: queries.DraftIssue{
+			DraftIssue: DraftIssue{
 				Title: "Pull Request title",
 				Body:  "a body",
 			},
 		},
 	}
-	draftIssue.FieldValues.Nodes = []queries.FieldValueNodes{
+	draftIssue.FieldValues.Nodes = []FieldValueNodes{
 		milestoneFieldValue,
 	}
-	p := &queries.Project{}
-	p.Fields.Nodes = []queries.ProjectField{milestoneField}
+	p := &Project{}
+	p.Fields.Nodes = []ProjectField{milestoneField}
 	p.Items.TotalCount = 5
-	p.Items.Nodes = []queries.ProjectItem{
+	p.Items.Nodes = []ProjectItem{
 		draftIssue,
 	}
 
-	out, err := json.Marshal(JSONProjectDetailedItems(p))
+	out, err := json.Marshal(p.DetailedItems())
 	assert.NoError(t, err)
 	assert.JSONEq(
 		t,
@@ -357,10 +361,3 @@ func TestJSONProjectItem_DraftIssue_ProjectV2ItemFieldMilestoneValue(t *testing.
 		string(out))
 
 }
-
-func TestCamelCase(t *testing.T) {
-	assert.Equal(t, "camelCase", camelCase("camelCase"))
-	assert.Equal(t, "camelCase", camelCase("CamelCase"))
-	assert.Equal(t, "c", camelCase("C"))
-	assert.Equal(t, "", camelCase(""))
-}
diff --git a/pkg/cmd/project/shared/queries/queries_test.go b/pkg/cmd/project/shared/queries/queries_test.go
index 5767929a3..bfdd4d95c 100644
--- a/pkg/cmd/project/shared/queries/queries_test.go
+++ b/pkg/cmd/project/shared/queries/queries_test.go
@@ -428,3 +428,10 @@ func TestProjectItems_FieldTitle(t *testing.T) {
 	assert.Equal(t, project.Items.Nodes[0].FieldValues.Nodes[0].ProjectV2ItemFieldIterationValue.Title, "Iteration Title 1")
 	assert.Equal(t, project.Items.Nodes[0].FieldValues.Nodes[1].ProjectV2ItemFieldMilestoneValue.Milestone.Title, "Milestone Title 1")
 }
+
+func TestCamelCase(t *testing.T) {
+	assert.Equal(t, "camelCase", camelCase("camelCase"))
+	assert.Equal(t, "camelCase", camelCase("CamelCase"))
+	assert.Equal(t, "c", camelCase("C"))
+	assert.Equal(t, "", camelCase(""))
+}