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
- 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
- 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
- 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
gsonList function to construct GSONList
object and corresponding print method implemented
(2022-09-06, Tue)
gson 0.0.8
- remove
iconv in write.gson as it may
return NA (2022-08-20, Sat)
gson 0.0.7
- add
keytype slot for read.gson() and
write.gson(). (2022-08-5, Wed)
- add
keytype slot for GSON object. (2022-07-13,
Wed)