@model ImportIndexViewModel @{ ViewData["Title"] = "JSON Import"; var preview = Model.Preview; }

Tools

JSON Import

Paste or upload a legacy JSON package or a PlotWeaver backup file, preview it, dry-run if needed, then import a new project.

Download sample JSON Back to projects
@if (!string.IsNullOrWhiteSpace(Model.StatusMessage)) {
Import paused

@Model.StatusMessage

@if (preview is { Package: not null, HasDuplicateProjectTitle: true }) {

Duplicate imports will be renamed to @preview.DuplicateImportProjectTitle.

} @if (!string.IsNullOrWhiteSpace(Model.TechnicalDetail)) {
Technical detail

@Model.TechnicalDetail

}
}
Uploading a file replaces the pasted text for this preview.
Supported JSON schema

Required

project.title, each book.title, each book.seriesOrder, each chapter.title, each chapter.order, each scene.title, and each scene.order.

Optional

packageVersion, source, createdBy, createdDate, description, top-level aliases, top-level characters, top-level locations, top-level plotLines, top-level storyAssets, top-level sceneDependencies, plot line threads, thread events, asset states, asset events, initial asset owner/location mapping, POV/location mapping, date/time text, purpose/outcome text, and sceneCharacters.

Dry run and aliases

Dry run performs validation and mapping diagnostics without saving data. aliases.characters and aliases.locations map imported names to top-level names before matching. Matching is case-insensitive, trims whitespace, normalizes repeated spaces, and uses punctuation-insensitive matching only when safe.

Scene links

sceneDependencies use fromScene and toScene objects. Link scenes with bookOrder, chapterOrder, and sceneOrder; sceneRef is optional future-friendly text. dependencyType, strength, description, and notes are optional. Warnings can be acknowledged; errors block import. Unknown fields are ignored safely.

Not imported yet

No AI extraction, Word/text parsing, manuscript parsing, metrics import, continuity warning import, relationship graphs, merge/update, or export logic. This importer is ready for manuscript-derived JSON, and unknown fields are ignored safely.

@if (preview is not null) {

Preview

@(preview.Package?.Project.Title ?? "Import package")

@preview.BookCount books @preview.ChapterCount chapters @preview.SceneCount scenes @preview.CharacterCount characters @preview.LocationCount locations @preview.SceneCharacterAppearanceCount appearances @preview.PlotLineCount plot lines @preview.PlotThreadCount threads @preview.ResolvedThreadEventCount events @preview.StoryAssetCount assets @preview.ResolvedAssetStateCount asset states @preview.ResolvedAssetEventCount asset events @preview.ResolvedAssetCustodyCount custody links @preview.ResolvedSceneDependencyCount dependencies @preview.ErrorCount errors @preview.WarningCount warnings @preview.UnresolvedReferenceCount unresolved @preview.AutoCorrectionCount corrections @preview.SkippedItemCount skipped
Import health
@preview.ErrorCount errors @preview.WarningCount warnings @preview.AutoCorrectionCount auto-corrections @preview.SkippedItemCount skipped items @preview.UnresolvedReferenceCount unresolved references
@if (preview.Package is not null && (!string.IsNullOrWhiteSpace(preview.Package.Source) || !string.IsNullOrWhiteSpace(preview.Package.CreatedBy) || !string.IsNullOrWhiteSpace(preview.Package.CreatedDate) || !string.IsNullOrWhiteSpace(preview.Package.Description))) {

Source: @(preview.Package.Source ?? "Not supplied") / Created by: @(preview.Package.CreatedBy ?? "Not supplied") / Created date: @(preview.Package.CreatedDate ?? "Not supplied") @if (!string.IsNullOrWhiteSpace(preview.Package.Description)) { / @preview.Package.Description }

}
@* Carry forward the active textarea JSON for secondary forms. The visible textarea is authoritative. *@
@if (preview.Validation.Errors.Any()) {
Errors block import
} @if (preview.Validation.Warnings.Any()) {
Warnings require confirmation
} @if (preview.AutoCorrections.Any() || preview.Validation.AutoCorrections.Any() || preview.SkippedItems.Any() || preview.Validation.SkippedItems.Any() || preview.AliasWarnings.Any()) {
Health details @if (preview.AutoCorrections.Any() || preview.Validation.AutoCorrections.Any()) { Auto-corrections: @string.Join("; ", preview.AutoCorrections.Concat(preview.Validation.AutoCorrections).Take(12)) } @if (preview.SkippedItems.Any() || preview.Validation.SkippedItems.Any()) { Skipped items: @string.Join("; ", preview.SkippedItems.Concat(preview.Validation.SkippedItems).Take(12)) } @if (preview.AliasWarnings.Any()) { Alias warnings: @string.Join("; ", preview.AliasWarnings.Take(12)) }
} @if (preview.Package is not null) { @if (preview.UnmatchedPovNames.Any() || preview.UnmatchedLocationNames.Any() || preview.UnmatchedSceneCharacterNames.Any() || preview.UnresolvedThreadEventReferences.Any() || preview.UnresolvedAssetReferences.Any() || preview.UnmatchedAssetOwnerNames.Any() || preview.UnmatchedAssetLocationNames.Any() || preview.UnresolvedSceneDependencyReferences.Any()) {
@if (preview.UnmatchedPovNames.Any()) { Unmatched POV: @string.Join(", ", preview.UnmatchedPovNames) } @if (preview.UnmatchedLocationNames.Any()) { Unmatched locations: @string.Join(", ", preview.UnmatchedLocationNames) } @if (preview.UnmatchedSceneCharacterNames.Any()) { Unmatched scene characters: @string.Join(", ", preview.UnmatchedSceneCharacterNames) } @if (preview.UnresolvedThreadEventReferences.Any()) { Unresolved thread events: @string.Join(", ", preview.UnresolvedThreadEventReferences) } @if (preview.UnresolvedAssetReferences.Any()) { Unresolved asset references: @string.Join(", ", preview.UnresolvedAssetReferences) } @if (preview.UnmatchedAssetOwnerNames.Any()) { Unmatched asset owners/events: @string.Join(", ", preview.UnmatchedAssetOwnerNames) } @if (preview.UnmatchedAssetLocationNames.Any()) { Unmatched asset locations: @string.Join(", ", preview.UnmatchedAssetLocationNames) } @if (preview.UnresolvedSceneDependencyReferences.Any()) { Unresolved dependencies: @string.Join(", ", preview.UnresolvedSceneDependencyReferences) }
}
@foreach (var bookPreview in preview.BookPreviews) { var book = bookPreview.Book;
Book @book.SeriesOrder: @(string.IsNullOrWhiteSpace(book.Subtitle) ? book.Title : $"{book.Title}: {book.Subtitle}") @bookPreview.ChapterCount chapters / @bookPreview.SceneCount scenes @foreach (var chapterPreview in bookPreview.ChapterPreviews) { var chapter = chapterPreview.Chapter;
Chapter @chapter.ChapterNumber: @chapter.Title @chapterPreview.SceneCount scenes
    @foreach (var scene in chapter.Scenes.OrderBy(x => x.Order)) {
  • Scene @scene.SceneNumber: @scene.Title POV: @(scene.PovCharacterName ?? "Not set") / Location: @(scene.LocationName ?? "Not set") / Characters: @scene.SceneCharacters.Count
  • }
}
}
@if (preview.Package.PlotLines.Any()) {
@foreach (var plotLine in preview.Package.PlotLines) {
Plot line: @(string.IsNullOrWhiteSpace(plotLine.DisplayName) ? plotLine.Name : plotLine.DisplayName) @plotLine.Threads.Count threads / @plotLine.Threads.Sum(x => x.Events.Count) events @foreach (var thread in plotLine.Threads) {
@(string.IsNullOrWhiteSpace(thread.DisplayName) ? thread.Name : thread.DisplayName) @thread.Events.Count events
}
}
} @if (preview.Package.SceneDependencies.Any()) {
@foreach (var group in preview.Package.SceneDependencies.Select((dependency, index) => new { Dependency = dependency, Index = index + 1 }).GroupBy(x => $"{x.Dependency.FromScene?.BookOrder?.ToString() ?? "?"}/{x.Dependency.FromScene?.ChapterOrder?.ToString() ?? "?"}").OrderBy(x => x.Key)) {
Dependencies from @group.Key @group.Count() links
    @foreach (var item in group.Take(12)) { var dependency = item.Dependency; var fromScene = $"{dependency.FromScene?.BookOrder?.ToString() ?? "?"}/{dependency.FromScene?.ChapterOrder?.ToString() ?? "?"}/{dependency.FromScene?.SceneOrder?.ToString() ?? "?"}"; var toScene = $"{dependency.ToScene?.BookOrder?.ToString() ?? "?"}/{dependency.ToScene?.ChapterOrder?.ToString() ?? "?"}/{dependency.ToScene?.SceneOrder?.ToString() ?? "?"}";
  • @fromScene -> @toScene @((dependency.DependencyType ?? "Dependency")) / @(dependency.Strength ?? "soft")
  • } @if (group.Count() > 12) {
  • Showing first 12 links in this group.
  • }
}
} @if (preview.Package.StoryAssets.Any()) {
@foreach (var asset in preview.Package.StoryAssets) {
Asset: @(string.IsNullOrWhiteSpace(asset.DisplayName) ? asset.Name : asset.DisplayName) @asset.States.Count states / @asset.Events.Count events
  • Type: @(asset.Type ?? "Not set") / Initial owner: @(asset.InitialOwnerCharacterName ?? "Not set") / Initial location: @(asset.InitialLocationName ?? "Not set")
}
}

Confirm import

This will create @preview.Package.Project.Title with @preview.BookCount books, @preview.ChapterCount chapters, @preview.SceneCount scenes, @preview.CharacterCount characters, @preview.LocationCount locations, @preview.SceneCharacterAppearanceCount scene character appearances, @preview.PlotLineCount plot lines, @preview.PlotThreadCount plot threads, @preview.ResolvedThreadEventCount resolved thread events, @preview.StoryAssetCount story assets, @preview.ResolvedAssetStateCount resolved asset states, @preview.ResolvedAssetEventCount resolved asset events, @preview.ResolvedAssetCustodyCount custody links, and @preview.ResolvedSceneDependencyCount scene dependencies.

@if (preview.HasDuplicateProjectTitle) {

Because this title already exists, the created project will be named @preview.DuplicateImportProjectTitle if you continue.

}
@* Carry forward the active textarea JSON for dry run/import. The visible textarea is authoritative. *@ @if (preview.Validation.Warnings.Any()) {
} @if (preview.HasDuplicateProjectTitle) {
}
}
} @section Scripts { }