NEWS
gson 0.2.2
- keep the documented columns of
gsid2gene, gsid2name and gene2name as character in gson(), so that a producer passing a factor no longer leaks it into the object, and a double column is stored by its digits -- as.character(1e6) prints "1e+06", so format(scientific = FALSE) is used for doubles. clusterProfiler::gson_GO() builds gsid2gene with utils::stack(), whose ind column is a factor, and a factor retains the levels it no longer uses: every split(gsid2gene$gene, gsid2gene$gsid) then invents one empty gene set per unused level -- for gson_GO("org.Hs.eg.db", ont = "CC") that is 4069 gene sets of which 2067 are empty, although only 2002 terms carry annotations -- and length(), names(), write.gmt() and write.gson() report those phantom sets as real ones (write.gmt() emits a gsid<TAB>description line with no gene, write.gson() a "gsid": [] entry). The phantoms reach the consumers through their own split(): enrichit::ora_gson(), gsea_gson(), nsea_gson() and DOSE::getGeneSet(). write.gson()/read.gson() also stopped round-tripping to an identical object, since JSON has no factor type and read.gson() coerced the columns back to character; that coercion now lives in one place, gson(), and the duplicated lines in read.gson() are gone (2026-10-08, Thu)
- check the structural contract of the three mapping tables when a
GSON is created, so an object assembled with new("GSON", ...) is refused by validObject() when a table is not a data.frame, lacks the documented columns, or holds identifiers that are not character. Only the structure is enforced here: duplicated identifiers, missing cross-references between the tables, empty tables and non-scalar metadata stay with validate_gson(), which reports them instead of refusing them, and objects that violate those rules must remain constructible so that they can be inspected (2026-10-08, Thu)
write.gmt() and write.gson() now skip memberships that carry no gene set ID or no gene, and warn about how many were omitted. An empty gsid used to abort write.gmt() with subscript out of bounds, and a missing gene used to reach the output as the literal NA -- a GMT field of NA, a [null] member in the JSON -- which the consumers read back as a gene genuinely called NA and hand on to the enrichment routines. write.gmt() also falls back to the gene set ID when gsid2name holds no usable name for a set, so the description column can no longer be the string NA either. Objects whose memberships are all usable are written unchanged and silently, and write.gson()/read.gson() still round-trip them to an identical object (2026-10-08, Thu)
gson() also drops the row labels the producers leave on the three mapping tables, so that an object whose memberships are already grouped by gene set ID reads back from write.gson()/read.gson() as the object it was. clusterProfiler::gson_GO("org.Hs.eg.db", ont = "CC") rows its gsid2name by GO term and inherits for gsid2gene the row indices of the annotation table it was subset from, while JSON carries no row names and read.gson() can only produce the default sequence: identical() of the two objects was FALSE for that 2002-set, 378845-membership object even though every value matched, and it is TRUE now. The labels repeat what the columns already hold, so dropping them gives up nothing. What the format still cannot carry is the row order of gsid2gene, since a .gson file keys the memberships by gsid and a table that was not already grouped by gene set ID comes back regrouped -- the gene sets and the order of the genes within each are preserved (2026-10-08, Thu)
GSON gains gsidtype, the identifier domain the gene set IDs are drawn from (GO, KEGG, REAC, ...), so that a consumer can stop inferring it from the shape of the identifiers -- GOSemSim::parse_gson() currently repairs a missing GO: prefix and rejects a collection with no GO ID found by regular expression, which is exactly the guess this field replaces. It sits beside keytype, which describes the genes rather than the gene set IDs, and is threaded through gson(), read.gson(), write.gson(), as.list(), validate_gson() and print(). It is free text on purpose: validate_gson() asks only for one value or NULL, because an enforced enum would reject every producer that names its domain differently. Files written before the field existed read it back as NULL, and a file that carries it still opens in the released 0.2.1, which ignores the unknown key (verified by reading a gsidtype file with the CRAN build). schema_version is not bumped for it -- gson() still writes 1.0 and a producer that wants to declare 1.1 says so -- but validate_gson() now reports a schema_version it does not recognise instead of staying silent, and never rewrites the value (2026-10-08, Thu)
urlpattern can now be consumed: gson_url() returns the URLs of the gene sets, one per ID and defaulting to every names(x) of the object, and browseGS() opens them in a browser. Both were reachable before this release only from inside the package -- browseGS() was never exported, and its sub() call passed the pattern as the string to search, so a vector of gene set IDs collapsed to a single URL -- and browseGS() on an object without a urlpattern computed sub(NULL, ...) into character(0), which browseURL() silently opened nowhere. It is now an error, and gson_url() warns and returns NA instead. The URL comes only from what the object declares: gson ships no table of other databases' websites, so a producer that wants its gene sets browsable declares a urlpattern (read.gmt.wp() does, and ReactomePA::gson_Reactome() currently passes urlpattern = NULL). set_gson(), referenced nowhere in the family, is gone (2026-10-08, Thu)
GSON objects can be subset with [: x[i] returns the gene sets selected by i as a GSON. Before, taking a subset meant filtering the three mapping tables by hand -- gson(x@gsid2gene[x@gsid2gene$gsid %in% ids, ]) -- and keeping gsid2name and gene2name in step with gsid2gene oneself, which is easy to get wrong in the direction validate_gson() reports: a gene2name row for a gene that is no longer a member of any gene set. The method prunes the three tables together, filtering gene2name by the genes that survive, and carries every metadata slot (species, gsname, version, accessed_date, keytype, gsidtype, urlpattern, info, schema_version) over to the result. Gene set IDs, positions and logical vectors all select, x[] returns x, and a gene set ID the object does not hold or an index out of bounds is an error that names what is missing. x[, j] is an error too, since a GSON has no second dimension. x[[i]] is unchanged and still returns the genes of a single gene set (2026-10-08, Thu)
gson_union() merges several GSON objects, or one GSONList, into a single collection -- the multi-source case (GO + KEGG + REAC as one membership table) that callers used to assemble from rbind() and unique() themselves -- and a GSONList now survives subsetting, since x[1] used to come back as a plain list and lost print.GSONList. Two objects that name the same source (gsname, and gsidtype too once every object declares one) are the same collection: a gene set both of them carry is joined into one set holding every gene either gives it, so two files of one download merge rather than collide. A gene set ID that two different sources both use is refused, because the merged table would say that one ID names two sets of genes and every result read from it would be quietly wrong; .conflict = "prefix" qualifies every ID with its source (KEGG:hsa00010) and "rename" qualifies only the colliding ones, leaving IDs that already begin with their source alone, and the gsid2name rows follow the IDs they describe. Of the metadata, species, keytype and schema_version have to agree -- one membership table cannot mean two organisms, two identifier types or two file schemas -- while gsname, gsidtype, version, accessed_date and info are joined with " + " when the sources name themselves differently. urlpattern is kept only when every object declares the same one and no ID moved, because a pattern that resolves part of the collection to the wrong page is worse than no pattern (2026-10-08, Thu)
read.gson() says which file is wrong and what about it, instead of passing on whatever jsonlite or utils::stack() happened to notice. A .gson file is written by other programs as often as by this one, and a file that did not follow the format used to fail with a message about R internals rather than about the file: at least one vector element is required when gsid2gene was absent, invalid 'times' value when it held a JSON array of arrays, arguments imply differing number of rows: 1, 0 when it held a string, $ operator is invalid for atomic vectors when the whole file was an array, gsid2name should have at least 2 columns when gsid2name held a string instead of the two arrays gsid and name, and lexical error: invalid char in json text -- quoting the path of a file that does not exist -- from jsonlite. Two malformed files read without any error at all: {"gsid2gene": {"": ["101"]}} gave a gene set with an empty ID, and {"gsid2gene": {"GS1": {"a": ["101"]}}} gave GS1 -> 101 with the inner key silently dropped (verified against the released 0.2.1). Each case is now an error naming the file, the member and the shape found -- the gson file <path> holds 'gsid2gene' as a matrix, but the memberships must be an object keyed by gene set ID, e.g. {"GS1": ["101", "102"]} -- a file that will not parse at all reports the gson file <path> could not be read: with the parse error beneath it, and a local path that does not exist is caught before jsonlite is asked (a URL is not, so reading from the web still works). The shapes recognised are the ones jsonlite::fromJSON() actually produces, which simplifies an object of two equal-length arrays to a data.frame and an array of equal-length arrays to a matrix. An empty "gsid2gene": {} is no longer an error either: it is a collection of no gene sets, which is what write.gson() of an empty GSON writes, and the two now round-trip to an identical object. Well-formed files, from this package or from another, read as before (2026-10-08, Thu)
validate_gson() reports what the identifier fields claim without carrying: a keytype or gsidtype left at a placeholder -- UNKNOWN, NA, null, none, -, the empty string -- now reads keytype 'UNKNOWN' names no identifier type; leave it NULL when the type is not known, and a gsidtype the object's own gene set IDs contradict reads gsidtype 'KEGG' is not the prefix of the gene set IDs, which begin GO -- as in 'GO:0008150'. There is deliberately no dictionary of accepted values, which is what the same report would need to be complete: MicrobiomeProfiler writes kegg_orthology, kegg_compound, kegg_enzyme, eggNOG_OG, taxid, cog, metabolite_id and hmdb_id as keytype, none of them a keytypes() value of any Bioconductor annotation package, so an enumerated list held by gson would reject the collections the family itself produces and could only be kept current by hand. What gson can verify is an object against itself, so only a prefix the IDs actually carry is compared: identifiers with no prefix (WP5119, hsa00010, the numeric GO IDs some producers write) give nothing to check and are left alone, a gsidtype that gson_union() joined ("GO + KEGG") is read as the several domains it is, and IDs qualified with the source's own gsname -- what .conflict = "prefix" writes -- count as agreement, as does one domain out of several, since a partly right claim is a merged collection rather than a wrong one. Nothing is rewritten, following the unknown schema_version: keytype = "UNKNOWN" is the value enrichit::ora_gson() and gsea_gson() give an enrichResult when a GSON declares none, so it is a value in circulation, and silently turning it into something else would change what a consumer asks an annotation package. validate_gson() throws on a report only when called with the default error = TRUE, and nothing outside gson calls it (verified by searching the family); the collections this package ships stay valid, read.gmt.wp()'s bundled WikiPathways object with gsidtype = "WP" and IDs like WP5119 among them (2026-10-08, Thu)
write.gson() writes the gene sets in the order the collection holds them, so a .gson file reads back as the object it was written from. split() sorted the gene set IDs, and a JSON object keeps the order it is written in, so every collection whose own order is not the alphabetical one came back rearranged: the bundled WikiPathways object, whose names() begin WP5119, WP2840, WP4685, read back beginning WP100, WP106, WP107, and identical() of the two was FALSE although all 724 gene sets and all 30629 memberships matched. The order is not decoration -- names(x), print() and x[1] all follow it, so a ranked collection loses what its rank means -- and keeping it costs nothing, since jsonlite::toJSON() writes the members of an object in the order the list holds them (2026-10-08, Thu)
- An audit of this round's own changes found three gaps at the boundaries it had just added, now closed.
read.gson() refused a file holding "GS2": [] with the misleading "not a flat list of identifiers" -- jsonlite reads an empty array as an empty list, so an empty gene set fell into the wrong check -- and now says holds gene set 'GS2' with an empty gene list; a GSON cannot hold a set of no genes, and write.gson() omits such sets with a warning, since dropping the set in silence would look like the file never mentioned it. A hand-written "GS1": ["g1", null] read without complaint into a gene genuinely called NA -- the same disease the write side skips such rows for -- and the read side now skips them on the same terms, warning how many memberships were omitted; a set whose every gene is unusable leaves a collection without it. And validate_gson() silently took a keytype or gsidtype left at NA for absent: an NA slot is not NULL, and it is now reported with the values that name nothing. Files without these defects read exactly as before (2026-10-08, Thu)
Description describes what a collection is used for rather than what the object holds: a GSON can be built from data frames, read from and written to the 'gson' JSON and 'GMT' formats, subset by gene set, merged across sources, validated, and resolved to the web addresses of the databases it comes from, which is the point of the format -- a collection gathered by one package is analysed by another. URL and BugReports were missing, so the manual pages and the CRAN page had no link out; they now follow the family convention and point at the book and at YuLab-SMU/gson. Three dependencies that no code reached are gone: rlang was imported for a @importFrom rlang .data tag left over from the tidyr::separate() call 0.2.1 replaced with strsplit(), utils::packageVersion() for the commented-out info line of write.gson(), and digest and fs were listed under Suggests with no reference anywhere in the package or its tests. Of the three, only rlang was under Imports and so was actually loaded -- an importFrom directive pulls the namespace in when gson is loaded -- while digest and fs sat under Suggests and cost nothing but a line of metadata. gson now starts no package its code does not call. R CMD check is unchanged and a GSON built and written before the trim reads identically after it (2026-10-08, Thu)
README.Rmd is the source of the readme, and README.md is what it renders, following the convention of the family: the badges, the opening paragraph and the output of every example come out of the package rather than out of memory, so the readme cannot quietly fall behind the code it describes. The readme: target of the Makefile already pointed at this file, which did not exist. It walks through building a collection from two data frames, the .gson round trip and what a file that cannot be read is answered with, write.gmt() and both GMT readers, gson_url() and browseGS() and the object that has no urlpattern to browse, [ by ID and by position and by name, gson_union() with both .conflict modes and the collision it refuses, validate_gson() with the report it makes, and the JSON a collection is stored as. Every chunk that runs was run, and the output beside it is what this build printed; no example prints a temporary path, so rendering it twice gives the same README.md; the enrichit calls are shown as an example rather than executed, since gson does not depend on enrichit to be rendered. README.Rmd is in .Rbuildignore, so the tarball ships README.md as before and nothing else about the package changes (2026-10-08, Thu)
gson 0.2.1 (2026-08-04)
- replace
tidyr::separate with base R strsplit in read.gmt.wp(), resolving vector memory limit / long vectors not supported errors on macOS aarch64 and removing tidyr dependency (2026-08-04, Tue, clusterProfiler#762)
gson 0.2.0 (2026-07-01)
- expand regression tests for empty memberships, duplicate mappings, missing metadata, multi-species metadata, and invalid container access (2026-06-30, Tue)
- add
schema_version to the GSON object and JSON representation to support future file schema evolution (2026-06-30, Tue)
- add container helpers for GSON objects, including
as.data.frame(), length(), names(), [[, and write.gmt() (2026-06-30, Tue)
- add
validate_gson() to check the core GSON data contract and optional mapping table consistency (2026-06-30, Tue)
- add README documentation describing GSON as a gene set collection container with creation, GSON IO, GMT, and enrichment examples (2026-06-30, Tue)
- fix
read.gson()/write.gson() round-tripping of missing optional fields and preserve gsid2gene character columns (2026-06-30, Tue)
- normalize the first two columns of GSON mapping tables in
gson() and add input validation (2026-06-30, Tue)
- make
gsonList() robust to GSON objects without gsname and add testthat regression tests (2026-06-30, Tue)
- introduce new slot 'urlpattern' for generating URL of selected gene set (2023-08-16, Wed)
- use
yulab.utils::yread() to read GMT file and gson file (2023-08-16, Wed)
gson 0.1.0 (2023-03-07)
- fixed
read.gson() by setting the default values (if empty) to NULL (to make it compatible with 'clusterProfiler') (2023-03-03, Mon)
- set
gsname="WikiPathways" in read.gmt.wp(output = "gson") (2022-09-23, Fri)
gson 0.0.9 (2022-09-06)
gsonList function to construct GSONList object and corresponding print method implemented (2022-09-06, Tue)
gson 0.0.8 (2022-08-20)
- remove
iconv in write.gson as it may return NA (2022-08-20, Sat)
gson 0.0.7 (2022-08-09)
- add
keytype slot for read.gson() and write.gson(). (2022-08-5, Wed)
- add
keytype slot for GSON object. (2022-07-13, Wed)