What does Crossref add to our journal data?

Meeting update · 15 September 2026

R code
library(arrow)
library(dplyr)
library(jsonlite)
fmt <- function(x) format(x, big.mark = ",", scientific = FALSE, trim = TRUE)

journals <- read_parquet(params$data_file)
crossref <- readRDS(params$crossref_cache)

We use the existing PKP V7 list enriched with OpenAlex and Crossref: 98,273 records, including 72,084 with a usable ISSN. The saved Crossref data were collected on 2026-08-03. The results below are calculated from that merged file when this report runs.

1. Crossref fields

Agreed task: Compare every Crossref source field with related OpenAlex fields, using actual values from the same journal.

R code
example <- journals |>
  filter(openalex_id == "https://openalex.org/S4210240683",
         openalex_match_status == "unique",
         crossref_match_status == "unique")
stopifnot(nrow(example) == 1,
          example$crossref_title == example$openalex_display_name,
          setequal(fromJSON(example$crossref_ISSN),
                   fromJSON(example$openalex_issn)))

The example throughout both tables is International Journal of Innovative Research and Scientific Studies (ISSNs 2790-7600 and 2617-6548). All 11 top-level fields from the saved Crossref /journals response are shown below. Nested fields contain further entries. Crossref’s article-level /works endpoint provides additional information such as authors, abstracts and references; those article records are not part of this journal table. The examples are read from this one row of the merged file. Long nested fields show selected entries; Full saved value opens the complete content. NA means the corresponding field exists but has no value for this journal.

R code: field names and explanations
field_map <- tibble::tribble(
  ~cr, ~cr_meaning, ~oa, ~oa_meaning,
  "title", "Journal name.",
  "display_name", "Journal name.",
  "publisher", "Publisher name.",
  "host_organization_name", "Publisher or hosting organisation name.",
  "ISSN", "Journal ISSNs.",
  "issn", "Journal ISSNs, without print/online labels.",
  "issn-type", "ISSNs with print or electronic labels.",
  "issn", "Same identifiers; no medium labels in this list.",
  "counts", "DOI counts: total, current and backfile publications.",
  "works_count", "Number of works recorded by OpenAlex.",
  "breakdowns", "DOI counts by publication (issued) year.",
  "counts_by_year", "Works published and citations received in each year.",
  "subjects", "Legacy subject codes, retired by Crossref in April 2024.",
  "topics", "OpenAlex topic labels and associated work counts.",
  "coverage", "Share of records containing each kind of metadata, for current and backfile publications.",
  "", "",
  "coverage-type", "The same coverage measures grouped by all, current and backfile publications.",
  "", "",
  "flags", "Yes/no indicators for deposited content and metadata.",
  "", "",
  "last-status-check-time", "Crossref status-check timestamp.",
  "", ""
)
cr_fields <- field_map$cr
available_cr_fields <- sort(unique(unlist(lapply(crossref$journals, names))))
stopifnot(setequal(cr_fields, available_cr_fields),
          all(paste0("crossref_", cr_fields) %in% names(example)),
          all(paste0("openalex_", field_map$oa[field_map$oa != ""]) %in%
                names(example)))
R code: select entries from nested fields
doi_counts <- fromJSON(example$crossref_counts)
cr_years <- fromJSON(example$crossref_breakdowns)[["dois-by-issued-year"]]
oa_years <- fromJSON(example$openalex_counts_by_year)
oa_topics <- fromJSON(example$openalex_topics)
cr_coverage <- fromJSON(example$crossref_coverage)
coverage <- fromJSON(example[["crossref_coverage-type"]])$all
cr_flags <- fromJSON(example$crossref_flags)

# Use the same year for the DOI and work counts.
comparison_year <- 2025
cr_year_count <- cr_years[cr_years[, 1] == comparison_year, 2]
oa_year <- oa_years[oa_years$year == comparison_year, ]
stopifnot(length(cr_year_count) == 1, nrow(oa_year) == 1)

# These excerpts are computed from the saved values; the full values remain available.
excerpts <- list(
  crossref_breakdowns = list(`dois-by-issued-year` =
    list(c(comparison_year, cr_year_count))),
  openalex_counts_by_year = oa_year,
  openalex_topics = head(oa_topics[c("display_name", "count")], 3),
  crossref_coverage = cr_coverage[c("abstracts-current", "references-current",
                                   "orcids-current")],
  `crossref_coverage-type` = list(all = coverage[c("abstracts", "references",
                                                  "orcids")]),
  crossref_flags = cr_flags[c("deposits-abstracts-current",
                             "deposits-references-current",
                             "deposits-orcids-current")]
)
R code: display the comparison
field_cell <- function(prefix, field, meaning) {
  if (field == "") return("")
  paste0("<code>", prefix, field, "</code><br>",
         htmltools::htmlEscape(meaning))
}
value_cell <- function(column) {
  if (column == "openalex_") return("")
  value <- example[[column]][[1]]
  if (is.na(value)) return("<code>NA</code> (not supplied)")
  value <- as.character(value)
  full <- if (jsonlite::validate(value)) prettify(value, indent = 2) else value
  full <- paste0("<pre class='field-value'>",
                 htmltools::htmlEscape(full), "</pre>")
  if (is.null(excerpts[[column]])) return(full)
  preview <- toJSON(excerpts[[column]], auto_unbox = TRUE, pretty = TRUE)
  paste0("<pre class='field-value'>", htmltools::htmlEscape(preview),
         "</pre><details><summary>Full saved value</summary>",
         full, "</details>")
}
comparison <- data.frame(
  `Crossref column and meaning` = mapply(field_cell, "crossref_",
                                         field_map$cr, field_map$cr_meaning),
  `Crossref example` = vapply(paste0("crossref_", field_map$cr),
                              value_cell, character(1)),
  `OpenAlex column and meaning` = mapply(field_cell, "openalex_",
                                         field_map$oa, field_map$oa_meaning),
  `OpenAlex example` = vapply(paste0("openalex_", field_map$oa),
                              value_cell, character(1)),
  check.names = FALSE
)

Crossref fields without a counterpart in this OpenAlex table

The OpenAlex cells are left blank where no corresponding source field exists.

R code
knitr::kable(comparison[field_map$oa == "", ], format = "html", escape = FALSE,
             row.names = FALSE)
Crossref column and meaning Crossref example OpenAlex column and meaning OpenAlex example
crossref_coverage
Share of records containing each kind of metadata, for current and backfile publications.
{
  "abstracts-current": 1,
  "references-current": 0,
  "orcids-current": 0.7579
}
Full saved value
{
  "affiliations-current": 0,
  "affiliation-ror-ids-backfile": 0,
  "similarity-checking-current": 1,
  "descriptions-current": 0,
  "ror-ids-current": 0,
  "funders-backfile": 0,
  "licenses-backfile": 1,
  "funders-current": 0,
  "affiliations-backfile": 0,
  "resource-links-backfile": 0.995652173913044,
  "orcids-backfile": 0.526086956521739,
  "funder-ror-ids-current": 0,
  "update-policies-current": 0,
  "ror-ids-backfile": 0,
  "orcids-current": 0.757862990090478,
  "similarity-checking-backfile": 0.995652173913044,
  "references-backfile": 0,
  "descriptions-backfile": 0,
  "award-numbers-backfile": 0,
  "update-policies-backfile": 0,
  "licenses-current": 1,
  "award-numbers-current": 0,
  "abstracts-backfile": 1,
  "resource-links-current": 1,
  "affiliation-ror-ids-current": 0,
  "funder-ror-ids-backfile": 0,
  "abstracts-current": 1,
  "references-current": 0
}
crossref_coverage-type
The same coverage measures grouped by all, current and backfile publications.
{
  "all": {
    "abstracts": 1,
    "references": 0,
    "orcids": 0.737
  }
}
Full saved value
{
  "all": {
    "last-status-check-time": 1785718578452,
    "affiliations": 0,
    "abstracts": 1,
    "orcids": 0.736965895727166,
    "affiliation-ror-ids": 0,
    "licenses": 1,
    "references": 0,
    "funders": 0,
    "similarity-checking": 0.999607996863975,
    "funder-ror-ids": 0,
    "award-numbers": 0,
    "ror-ids": 0,
    "update-policies": 0,
    "resource-links": 0.999607996863975,
    "descriptions": 0
  },
  "backfile": {
    "last-status-check-time": 1785718578452,
    "affiliations": 0,
    "abstracts": 1,
    "orcids": 0.526086956521739,
    "affiliation-ror-ids": 0,
    "licenses": 1,
    "references": 0,
    "funders": 0,
    "similarity-checking": 0.995652173913044,
    "funder-ror-ids": 0,
    "award-numbers": 0,
    "ror-ids": 0,
    "update-policies": 0,
    "resource-links": 0.995652173913044,
    "descriptions": 0
  },
  "current": {
    "last-status-check-time": 1785718578452,
    "affiliations": 0,
    "abstracts": 1,
    "orcids": 0.757862990090478,
    "affiliation-ror-ids": 0,
    "licenses": 1,
    "references": 0,
    "funders": 0,
    "similarity-checking": 1,
    "funder-ror-ids": 0,
    "award-numbers": 0,
    "ror-ids": 0,
    "update-policies": 0,
    "resource-links": 1,
    "descriptions": 0
  }
}
crossref_flags
Yes/no indicators for deposited content and metadata.
{
  "deposits-abstracts-current": true,
  "deposits-references-current": false,
  "deposits-orcids-current": true
}
Full saved value
{
  "deposits-abstracts-current": true,
  "deposits-orcids-current": true,
  "deposits-affiliation-ror-ids-current": false,
  "deposits": true,
  "deposits-affiliations-backfile": false,
  "deposits-update-policies-backfile": false,
  "deposits-similarity-checking-backfile": true,
  "deposits-award-numbers-current": false,
  "deposits-resource-links-current": true,
  "deposits-ror-ids-current": false,
  "deposits-articles": true,
  "deposits-affiliations-current": false,
  "deposits-affiliation-ror-ids-backfile": false,
  "deposits-funders-current": false,
  "deposits-references-backfile": false,
  "deposits-funder-ror-ids-current": false,
  "deposits-ror-ids-backfile": false,
  "deposits-abstracts-backfile": true,
  "deposits-licenses-backfile": true,
  "deposits-award-numbers-backfile": false,
  "deposits-descriptions-current": false,
  "deposits-references-current": false,
  "deposits-resource-links-backfile": true,
  "deposits-descriptions-backfile": false,
  "deposits-orcids-backfile": true,
  "deposits-funders-backfile": false,
  "deposits-funder-ror-ids-backfile": false,
  "deposits-update-policies-current": false,
  "deposits-similarity-checking-current": true,
  "deposits-licenses-current": true
}
crossref_last-status-check-time
Crossref status-check timestamp.
1785718578452

The title and both ISSNs agree. Crossref adds the print/electronic labels and the publisher name, Innovative Research Publishing, which is missing from the related OpenAlex field for this journal. For subject information, Crossref supplies an empty list, while OpenAlex supplies topic labels.

Both sources describe publication volume, but count their own records. Crossref has 2,551 DOI registrations and OpenAlex has 2,526 works. In 2025, the corresponding figures are 2,055 and 2,030. These figures show a difference between the databases; identifying the individual publications behind it would require a publication-level comparison. Crossref’s current category covers the snapshot year and the preceding two years of publication; backfile covers earlier publications.

Crossref’s main additional information concerns what publishers deposit. For this journal, across all years, 100% of Crossref records contain abstracts, 0% contain references and 73.7% contain ORCIDs. The zero for references means references were not deposited with Crossref; it does not mean the papers contain no references. coverage, coverage-type and flags express related deposit information as proportions or yes/no indicators. The status-check timestamp is a record of Crossref’s check, not an additional characteristic of the journal’s research.

Field meanings follow the Crossref journal schema, Crossref coverage definitions and OpenAlex Source fields.

2. Records added by Crossref

Agreed task: Find records with Crossref information but no OpenAlex match, and show their number and examples.

We select records with one Crossref match and no OpenAlex match in the saved ISSN search. Records with several possible OpenAlex matches are counted separately.

R code
crossref_only <- journals |>
  filter(crossref_match_status == "unique",
         openalex_match_status == "unmatched")
n_uncertain <- sum(journals$crossref_match_status == "unique" &
                   journals$openalex_match_status == "ambiguous")

examples <- crossref_only |>
  transmute(Journal = crossref_title,
            ISSN = gsub("|", ", ", crossref_matched_issns, fixed = TRUE),
            `Registered DOIs` = vapply(crossref_counts,
              function(x) as.numeric(fromJSON(x)$`total-dois`), numeric(1)))
knitr::kable(examples, format.args = list(big.mark = ","))
Journal ISSN Registered DOIs
Usrotuna Journal of Islamic Family Law 3089-1272 5
Saqifah Jurnal Hukum Ekonomi Syariah 2548-4974 32
JBMP Jurnal Bhakti Muhammadiyah Papua 3124-1522 20
Research Journal of Human and Social Aspects 3006-9696, 3006-970X 7
Collection of Scientific Papers 2720-8257 25
Stout in Agriculture and Animal Science 2979-2835 2
African Journal of Multidisciplinary Research and Reviews 3142-5917, 3142-5925 1

Crossref adds a match where OpenAlex found none for 7 of the 72,084 records with a usable ISSN (0.010%). Its additional reach is therefore small in this PKP list; its main contribution is the extra information it provides about records already found in both sources. Another 104 records have one Crossref match but several possible OpenAlex matches. They need a separate identity check. The 26,189 records without a usable ISSN were not searched.

3. Empty lists and missing values

Agreed task: Check an actual empty-list example and show how it should be treated.

Here we inspect subjects, the Crossref subject-label field, for the first record listed above.

R code
example_issn <- strsplit(crossref_only$crossref_matched_issns[1],
                        "|", fixed = TRUE)[[1]][1]
raw_example <- Filter(function(x) example_issn %in% x$ISSN,
                      crossref$journals)[[1]]
raw_example$subjects
list()

Crossref retired its old subject codes on 23 April 2024, leaving the fields empty for compatibility. The empty list is already present in the saved API response, before merging. We represent this unavailable classification as missing in the analysis:

R code
raw_nonempty_subjects <- sum(vapply(crossref$journals,
  function(journal) length(journal$subjects) > 0, logical(1)))
matched <- journals |> filter(crossref_match_status == "unique")
matched <- matched |>
  mutate(subjects_clean = na_if(crossref_subjects, "[]"))

knitr::kable(data.frame(
  `Subject information` = c("Supplied", "Missing"),
  Records = c(sum(!is.na(matched$subjects_clean)),
              sum(is.na(matched$subjects_clean))),
  check.names = FALSE), format.args = list(big.mark = ","))
Subject information Records
Supplied 0
Missing 52,630

The original cache contains 168,907 journal records, of which 0 have nonempty subject labels. All 52,630 records with one Crossref match likewise have empty subject labels in the merged data. This is consistent with the retired Crossref classification, rather than evidence that the journals have no subject or that the merge removed their labels. These legacy codes came from a Scopus-based experiment; they were not subject labels deposited by publishers. Crossref’s explanation also makes clear that the empty fields are intentional. OpenAlex’s topics is a separate field and is populated in the example above.

4. BigQuery access

Agreed task: Test whether a query now runs and identify the permission needed.

R code
query_test <- system2(params$bq_cli, c(
  "--headless=true", "--project_id=insyspo", "--location=US",
  "--dataset_id=insyspo:userdb_yann_jyal", "query",
  "--use_legacy_sql=false", "--maximum_bytes_billed=1",
  shQuote("SELECT 1 AS access_test")), stdout = TRUE, stderr = TRUE)

The SELECT 1 test still fails in insyspo; the account needs BigQuery Job User (roles/bigquery.jobUser), which includes bigquery.jobs.create.


Tasks follow the meeting discussion (02:13–08:29 and 34:23–34:31), alongside the field-review request.