Core Concepts
GlobsFramework revolves around four interlocking abstractions: GlobType, Field, Glob, and Annotations. Understanding them unlocks the whole framework.
GlobType — the schema
A GlobType is the runtime schema for a family of data objects. It plays the
role that a Java Class plays for beans, but it is richer: it lists fields,
their types, and any metadata annotations. Unlike a Class, it can be
created dynamically at runtime without generating bytecode.
public interface GlobType extends Annotations {
String getName();
Field[] getFields();
Field getField(String name) throws ItemNotFound;
Field getField(int index); // declaration order — the fast path
<T extends Field> T getTypedField(String name);
Field[] getKeyFields();
Field findFieldWithAnnotation(Key key);
MutableGlob instantiate();
// from Annotations
Glob findAnnotation(Key key);
Stream<Glob> streamAnnotations();
}
Static definition (compile-time)
Declare fields as public static final members and assign them in a static
block from declareXxxField(...), which returns the typed Field.
Nothing is reflected: the holder class is just a convenient place to keep the fields.
public class OrderType {
public static final GlobType TYPE;
public static final LongField orderId;
public static final StringField customerName;
public static final DoubleField totalAmount;
public static final BooleanField shipped;
static {
GlobTypeBuilder b = GlobTypeBuilderFactory.create("Order");
orderId = b.declareLongField("orderId");
customerName = b.declareStringField("customerName");
totalAmount = b.declareDoubleField("totalAmount");
shipped = b.declareBooleanField("shipped");
TYPE = b.build();
}
}
Dynamic definition (runtime)
The same builder has a second, fluent family — addXxxField(...), which returns
the builder instead of the field. Use it to build a GlobType from a JSON
schema, a database result set, or any external source — no Java class needed:
GlobType orderType = GlobTypeBuilderFactory.create("Order")
.addLongField("orderId")
.addStringField("customerName", NamingField.UNIQUE_GLOB)
.addDoubleField("totalAmount")
.addBooleanField("shipped")
.build();
Field — typed accessor
A Field is both a key into a Glob's data and a carrier of type information.
Rather than accessing data by string name (map.get("price")), you use a
typed Field object. The compiler knows the return type; no cast is needed.
Built-in field types
Scalar fields
IntegerField LongField DoubleField BigDecimalField StringField BooleanField BytesField DateField DateTimeField
Array fields
BooleanArrayField IntegerArrayField LongArrayField DoubleArrayField BigDecimalArrayField StringArrayField
Nested Globs
GlobField<T> — embed a single child Glob
GlobArrayField<T> — embed a list of child Globs
GlobUnionField — embed a polymorphic Glob
GlobArrayUnionField — a list of polymorphic Globs
The two first carry a type parameter naming the child's holder class — see TGlob.
Field is a sealed interface: the list above is exactly what it
permits, which is what lets a visitor be exhaustive.
The visitor pattern
Generic code — such as a serializer — uses the visitor pattern to dispatch on field type
without instanceof chains or reflection:
for (Field field : globType.getFields()) {
field.accept(new FieldVisitor.AbstractWithErrorVisitor() {
public void visitString(StringField f) {
String val = glob.get(f);
// write string to output...
}
public void visitDouble(DoubleField f) {
Double val = glob.get(f);
// write number to output...
}
public void visitGlob(GlobField f) {
Glob child = glob.get(f);
// recurse...
}
});
}
This is exactly how the JSON, XML, and binary serializers in the ecosystem are built —
and why you can add a new GlobType without touching them.
Glob — the data container
A Glob is an instance of a GlobType: it holds values for each
field and always knows its own type. Unlike a raw Map<String,Object>,
access is type-safe and the schema is always available.
Reading values
Glob g = /* from JSON / DB / HTTP ... */;
// Null-safe typed access
String name = g.get(OrderType.customerName); // String or null
double amount = g.get(OrderType.totalAmount, 0.0); // default if null
Optional<Double> opt = g.getOpt(OrderType.totalAmount);
// Know whether a field was set vs. not present
boolean hasValue = g.isSet(OrderType.shipped);
boolean isNull = g.isNull(OrderType.shipped);
// Access the schema at any time
GlobType type = g.getType();
MutableGlob — building & updating
MutableGlob order = OrderType.TYPE.instantiate()
.set(OrderType.orderId, 1001L)
.set(OrderType.customerName, "Alice")
.set(OrderType.totalAmount, 249.99);
// Update later
order.set(OrderType.shipped, true);
// Remove a value (goes back to "unset" state)
order.unset(OrderType.totalAmount);
TGlob — carrying the child's type along
A Glob is untyped in the Java sense: whatever field you read it from, you get a
Glob. For nested data that loses a piece of information the declaration knew —
which type is in there. GlobField<T> and
GlobArrayField<T> keep it, as a type parameter naming the target's holder
class, inferred from the variable you assign the field to:
public class OrderType {
public static final GlobType TYPE;
public static final GlobField<AddressType> shippingAddress;
public static final GlobArrayField<LineItemType> items;
static {
GlobTypeBuilder b = GlobTypeBuilderFactory.create("Order");
shippingAddress = b.declareGlobField("shippingAddress", () -> AddressType.TYPE);
items = b.declareGlobArrayField("items", () -> LineItemType.TYPE);
TYPE = b.build();
}
}
get(field) drops that parameter and hands back a bare Glob — or
null, which is on you to check. getT(field) keeps it, wrapping the
value in a TGlob<T> / TGlobArray<T> that also carries
the null handling:
Glob order = /* from JSON / DB / HTTP ... */;
// Untyped, null-checked by hand
Glob raw = order.get(OrderType.shippingAddress);
// Typed wrapper — the <AddressType> travels with the value
TGlob<AddressType> address = order.getT(OrderType.shippingAddress);
if (address.isPresent()) {
String city = address.get().get(AddressType.city);
}
Glob sure = address.notNull(); // fails here, not three frames later
Optional<Glob> opt = address.optional();
// Arrays: at(i), stream(), data() — and the same null helpers
TGlobArray<LineItemType> items = order.getT(OrderType.items);
double total = items.stream()
.mapToDouble(i -> i.get(LineItemType.unitPrice) * i.get(LineItemType.qty))
.sum();
TGlobCollection<T> is the same record over a Collection<Glob>.
No accessor returns one — build it yourself with TGlobCollection.of(...) when a
method hands a group of Globs around and you want the type to travel with them.
T is a marker:
nothing at runtime reads it, and the value inside stays an ordinary Glob, so the
field reads on it are still address.get().get(AddressType.city) — unchecked
against T. What you gain is at the signature level: a method taking a
TGlobArray<LineItemType> says which Globs it wants, and passing it an order
instead of its line items no longer compiles. All three records are marked
experimental in the source. TGlob and TGlobArray are
available since globs 5.7.0, TGlobCollection since 5.10.0.
Annotations — metadata as Globs
In GlobsFramework an annotation is not a Java @interface. It is a
GlobType of its own, and an annotation instance is a Glob. That means metadata
can carry structured, typed data, be created at runtime, and be queried like any other
Glob — far beyond what Java's @Annotation can express.
What an annotation looks like
One annotation is one class. It exposes its GlobType, a Key used
to look it up, and — when it carries no value — a single shared instance:
public class Required {
public static final GlobType TYPE;
public static final Key UNIQUE_KEY; // what you look it up with
public static final Glob UNIQUE_GLOB; // the valueless instance
static {
GlobTypeBuilder b = GlobTypeBuilderFactory.create("Required");
TYPE = b.build();
UNIQUE_KEY = KeyBuilder.newEmptyKey(TYPE);
UNIQUE_GLOB = TYPE.instantiate();
}
}
// An annotation with values is just a type with fields, plus a create() helper:
// MaxSize.create(255) -> a Glob of MaxSize.TYPE with VALUE = 255
@interface
named Foo_ mirroring the type, read back by a reflective type loader. Both are
gone from every repository. There is one file per annotation, and no reflection anywhere.
Attaching an annotation
An annotation takes effect only when its Glob is passed to
declareXxxField / addXxxField — the same call in a static holder
and in a dynamically built type:
// Dynamic type
GlobType product = GlobTypeBuilderFactory.create("Product")
.addStringField("title", NamingField.UNIQUE_GLOB, MaxSize.create(255))
.addDoubleField("price", Required.UNIQUE_GLOB)
.build();
// Static holder — same annotations, same call
public class ProductType {
public static final GlobType TYPE;
public static final StringField title;
public static final DoubleField price;
static {
GlobTypeBuilder b = GlobTypeBuilderFactory.create("Product");
title = b.declareStringField("title", NamingField.UNIQUE_GLOB, MaxSize.create(255));
price = b.declareDoubleField("price", Required.UNIQUE_GLOB);
TYPE = b.build();
}
}
A type itself can be annotated too, with builder.addAnnotation(...) — that is
how globs-sql carries a table name, for instance.
Querying annotations at runtime
// Find which field is "the naming field" — without hardcoding the field name
Field namingField = glob.getType()
.findFieldWithAnnotation(NamingField.KEY);
String label = (String) glob.getValue(namingField); // "XPhone", "Order #1001", ...
// On a single field: is it there, and what does it carry?
if (field.hasAnnotation(Required.UNIQUE_KEY)) { /* ... */ }
Glob maxSize = field.findAnnotation(MaxSize.KEY);
if (maxSize != null) {
int max = maxSize.get(MaxSize.VALUE);
}
Annotations are the mechanism behind most framework features: marking the key fields of a
database table, the field number in a binary or protobuf encoding, an option name on the
command line, a description in an OpenAPI or JSON Schema document. Each module declares its
own set and lists them in a registry type (AllCoreAnnotations,
AllJsonAnnotations, …).
Set vs. null — a subtle but important distinction
A Glob tracks two independent states for each field:
| State | isSet(f) | isNull(f) | Typical use |
|---|---|---|---|
| Never assigned | false |
true |
Field not present in source (e.g. missing JSON key) |
| Explicitly set to null | true |
true |
Source had a null / SQL NULL value |
| Set to a value | true |
false |
Normal non-null value |
This distinction is crucial in PATCH semantics (only serialize fields that were actually set) and in sparse data models (distinguish "field absent" from "field is null").