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.

What the parameter does, and what it does not. 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
Earlier versions shipped a second file per annotation — a Java @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:

StateisSet(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").