Chapter 12: Annotations and reflection
Introduces annotations as metadata on declarations and reflection as a way to inspect arbitrary classes at run time, using a JSON library to illustrate both.
After this chapter you will be able to
- Annotation metadata and semantics. Explain that annotations add metadata and semantics to declarations, such as marking a declaration deprecated or providing information for tools and libraries.
- Runtime inspection with reflection. Distinguish source code that names a specific class or function in advance from reflection, which lets code analyze declarations of classes not known in advance at run time.
- Annotation metadata and access. Explain that annotations associate additional metadata with declarations, and that tools can access it in source code, compiled class files, or at run time depending on the annotation’s configuration.
- Annotation placement on declarations. Describe how to apply an annotation to a Kotlin declaration and give an example of a declaration it can mark.
- Deprecated declarations and replacements. Explain what @Deprecated communicates and how its message, replacement, and level parameters affect use of a deprecated API.
- Annotation argument forms and constants. Describe the permitted kinds and syntax of annotation arguments, and explain why a property used as an argument may need const.
- Use-site annotation targets. Explain why a Kotlin declaration may need a use-site target and describe how to write one, including targeting a property's getter or setter.
- JSON serialization and deserialization. Describe how JKid converts a Kotlin object to JSON and back, and why deserialization requires an explicit class type argument.
- Annotation changes to JSON. Explain JKid's default property mapping and distinguish how @JsonName and @JsonExclude change it.
- Annotation class structure. Explain how to declare an annotation class and why it cannot have a body.
- Annotation constructor parameters. Describe where parameters of a Kotlin annotation class are declared and how they must be marked.
- Meta-annotations and valid targets. Explain what a meta-annotation is and how @Target restricts the declarations an annotation can mark.
- Annotation retention and reflection. Explain what @Retention controls and contrast Kotlin’s default retention with Java’s default.
- Class references in annotations. Explain how @DeserializeInterface uses a class reference to specify the concrete class JKid creates when deserializing an interface-typed property. Describe the CompanyImpl::class argument and the general ::class syntax.
- KClass parameter variance. Explain why a class-reference annotation parameter uses KClass<out Any> rather than KClass<Any> when it must accept CompanyImpl::class.
- Generic serializer class constraint. Explain why a generic serializer annotation parameter uses the pattern KClass<out ValueSerializer<*>>.
- Dynamic access through reflection. Explain how reflection differs from ordinary property and method access, and why a serializer needs it.
- Class inspection with KClass. Explain how to obtain a KClass for a class or object and use it to inspect declarations, including inherited nonextension properties.
- Reflective function calls and types. Distinguish the generic KCallable.call method from invoking a specifically typed KFunction, including how each handles arguments.
- Property getters through reflection. Describe how to retrieve top-level and member property values using the appropriate KProperty interface and its get method.
- Reflective property serialization. Explain how serializeObject uses reflection to serialize an object’s properties and why reading those properties yields values of type Any?.
- Default JSON value handling. Describe the JSON produced by default for an object’s properties and the roles of the helper functions in formatting it.
- Excluded property filtering. Explain how serialization omits properties marked with @JsonExclude by finding the annotation on reflected properties and filtering those properties out.
- Annotated JSON property names. Describe how @JsonName determines a property's JSON key and what happens when the annotation is absent.
- Custom serializer selection. Trace how a property with @CustomSerializer obtains a serializer, including the difference between an object and a regular class, and how its value is written.
- JSON deserialization stages. Trace how the lexer, parser, and deserialization component turn JSON text into an object.
- Seed-based object construction. Explain why ObjectSeed stores constructor arguments during parsing and how spawn completes construction, including nested values.
- Constructor invocation with defaults. Explain why deserialization uses KCallable.callBy instead of call and how its parameter-to-value map handles default arguments and JSON fields in any order.
- Constructor argument type conversion. Explain how JKid determines and supplies values of the right types for reflected constructor parameters, including numeric values.
- Cached constructor annotation data. Describe why ClassInfoCache and ClassInfo cache the relationship between constructor parameters, properties, annotations, JSON names, and serializers.
- Required parameter validation. Distinguish how ClassInfo handles missing optional, nullable, and other constructor parameters before creating an object.
What it builds on
- Getting rid of static utility classes: Top-level functions and properties (chapter 3) · The earlier discussion of top-level and object properties supports the const-property restriction.
- Associating data with a class and making it accessible: Properties (chapter 2) · Kotlin properties define accessors.
- Default parameter values (chapter 3) · Functions and constructors can have default parameter values.
- Companion objects: A place for factory methods and static members (chapter 4) · Companion objects provide a place for static-like members.
- (chapter intro) (chapter 4) · Basic Kotlin class declarations.
- Initializing classes: Primary constructor and initializer blocks (chapter 4) · Primary constructor declarations and val properties.
- Named arguments (chapter 3) · Named arguments in Kotlin function calls.
- Covariance preserves the subtyping relation (chapter 11) · Assumes the idea of covariance expressed with out.
Section by section
Introduction
Introduces annotations as metadata on declarations and reflection as a way to inspect arbitrary classes at run time, using a JSON library to illustrate both.
- Annotation metadata and semantics: Explain that annotations add metadata and semantics to declarations, such as marking a declaration deprecated or providing information for tools and libraries.
Why it matters: Annotations let declarations convey information beyond their ordinary code. - Runtime inspection with reflection: Distinguish source code that names a specific class or function in advance from reflection, which lets code analyze declarations of classes not known in advance at run time.
Why it matters: Reflection explains how a library can work with classes it was not written to name individually. - JKid serialization library example: Describe how the JKid JSON library illustrates the two features: reflection accesses properties and creates objects from JSON data, while annotations customize serialization and deserialization for particular classes and properties.
Why it matters: JKid gives a concrete reason to use annotations and reflection together.
Key terms: annotations, metadata, declarations, reflection, run time, arbitrary classes, JKid, JSON serialization, deserialization
12.1 Declaring and applying annotations
This section explains that annotations attach metadata to declarations and that their configuration determines where tools can access it.
- Annotation metadata and access: Explain that annotations associate additional metadata with declarations, and that tools can access it in source code, compiled class files, or at run time depending on the annotation’s configuration.
Why it matters: This explains what annotations provide before you encounter particular ways to apply or configure them.
Key terms: annotations, metadata, declaration
12.1.1 Applying annotations to mark declarations
This section shows how to apply Kotlin annotations and supply valid, compile-time arguments, using @Deprecated as a detailed example.
- Annotation placement on declarations: Describe how to apply an annotation to a Kotlin declaration and give an example of a declaration it can mark.
Why it matters: This is the basic syntax needed to recognize and use annotations in Kotlin code. - Deprecated declarations and replacements: Explain what @Deprecated communicates and how its message, replacement, and level parameters affect use of a deprecated API.
Why it matters: Deprecation can both warn API users and help them migrate to a replacement. - Annotation argument forms and constants: Describe the permitted kinds and syntax of annotation arguments, and explain why a property used as an argument may need const.
Why it matters: These restrictions explain which values you can put in an annotation and why an ordinary property may be rejected.
Key terms: annotation, declaration, @Test, @Deprecated, message, replaceWith, WARNING, ERROR, HIDDEN, annotation arguments
12.1.2 Specifying the exact declaration an annotation refers to: Annotation targets
This section explains how use-site targets place annotations on the intended declaration generated from Kotlin code and how annotations can affect the Java API.
- Use-site annotation targets: Explain why a Kotlin declaration may need a use-site target and describe how to write one, including targeting a property's getter or setter.
Why it matters: A use-site target lets you annotate the intended part of a property rather than leaving that choice implicit. - Available annotation target sites: Describe the kinds of elements use-site targets can select and state the placement rule for a file-targeted annotation.
Why it matters: The target names let you select a specific generated element, including one representing an entire file. - Java API annotation effects: Describe how the annotations named here change the Java-facing form of Kotlin declarations.
Why it matters: These annotations let Kotlin code present a different, more suitable API to Java callers. - Expression-level warning suppression: Explain how an annotation can apply to an expression and give the section's warning-suppression example.
Why it matters: This shows that annotations can address a warning locally rather than only marking larger declarations.
Key terms: use-site target, get, set, @JvmName, property, field, receiver, param, setparam, delegate
12.1.3 Using annotations to customize JSON serialization
This section shows how JKid converts Kotlin objects to and from JSON and how two annotations change the default JSON representation.
- JSON serialization and deserialization: Describe how JKid converts a Kotlin object to JSON and back, and why deserialization requires an explicit class type argument.
Why it matters: This establishes what the annotations change and what deserialization needs to reconstruct an object. - Annotation changes to JSON: Explain JKid's default property mapping and distinguish how @JsonName and @JsonExclude change it.
Why it matters: These annotations let a class control which properties appear in JSON and what keys represent them. - Default for excluded properties: Justify why an excluded property needs a default value in the Person example.
Why it matters: Excluding a property must not prevent JKid from creating the object when it reads the JSON back.
Key terms: serialize, deserialize, JSON representation, type argument, @JsonName, @JsonExclude, property names, keys, default value, deserialization
12.1.4 Creating your own annotation declarations
Explains how Kotlin annotation classes are declared, how their parameters work, and how applying them differs from applying Java annotations.
- Annotation class structure: Explain how to declare an annotation class and why it cannot have a body.
Why it matters: It distinguishes an annotation declaration from a class that contains behavior. - Annotation constructor parameters: Describe where parameters of a Kotlin annotation class are declared and how they must be marked.
Why it matters: These rules let you recognize and write parameterized annotation declarations. - Kotlin and Java annotation arguments: Distinguish Kotlin’s constructor-call syntax for applying annotations from the special treatment of value in Java annotations, including Java annotations used from Kotlin.
Why it matters: It helps you choose the right argument syntax when using Kotlin and Java annotations.
Key terms: annotation modifier, annotation class, metadata, primary constructor, val, named-argument syntax, value, JsonName
12.1.5 Meta-annotations: Controlling how an annotation is processed
This section explains how meta-annotations control where Kotlin annotations can be applied and whether they remain available at run time.
- Meta-annotations and valid targets: Explain what a meta-annotation is and how @Target restricts the declarations an annotation can mark.
Why it matters: Choosing a target prevents an annotation from being used on declarations its processor does not handle. - Java-compatible annotation targets: Describe why a Kotlin property-targeted annotation may also need a field target for use from Java.
Why it matters: The extra target lets the same annotation be used from Kotlin and Java code. - Annotation retention and reflection: Explain what @Retention controls and contrast Kotlin’s default retention with Java’s default.
Why it matters: Kotlin’s default lets code inspect annotations such as JKid’s through reflection without an explicit retention setting.
Key terms: meta-annotations, @Target, AnnotationTarget, ANNOTATION_CLASS, property target, AnnotationTarget.FIELD, @Retention, RUNTIME retention, reflection, .class file
12.1.6 Passing classes as annotation parameters to further control behavior
This section shows how a class reference passed to an annotation can choose an implementation for interface-typed properties during deserialization, and why the annotation parameter uses KClass<out Any>.
- Class references in annotations: Explain how @DeserializeInterface uses a class reference to specify the concrete class JKid creates when deserializing an interface-typed property. Describe the CompanyImpl::class argument and the general ::class syntax.
Why it matters: It explains how deserialization can create an object for a property whose declared type is an interface. - KClass parameter variance: Explain why a class-reference annotation parameter uses KClass<out Any> rather than KClass<Any> when it must accept CompanyImpl::class.
Why it matters: The out modifier lets the annotation accept a specific implementation class instead of only Any.
Key terms: @DeserializeInterface, CompanyImpl::class, ::class, class reference, KClass, KClass<CompanyImpl>, KClass<out Any>, out
12.1.7 Generic classes as annotation parameters
This subsection shows how to declare an annotation parameter that accepts a custom serializer class while restricting it to implementations of a generic serializer interface.
- Custom serializer annotation purpose: Explain why a property might use @CustomSerializer and what the supplied serializer does.
Why it matters: It explains why the annotation needs a reference to a serializer class. - Generic serializer class constraint: Explain why a generic serializer annotation parameter uses the pattern KClass<out ValueSerializer<*>>.
Why it matters: The type permits serializer classes as arguments without accepting unrelated classes such as Date.
Key terms: JKid, @CustomSerializer, ValueSerializer, KClass<out ValueSerializer<*>>, star projection
12.2 Reflection: Introspecting Kotlin objects at run time
Introduces reflection as a way to inspect objects at run time, explains its use in serialization, and identifies Kotlin and Java reflection options.
- Dynamic access through reflection: Explain how reflection differs from ordinary property and method access, and why a serializer needs it.
Why it matters: This explains why reflection is useful when code cannot name the declarations it will access ahead of time. - Kotlin and Java reflection APIs: Describe what the Kotlin reflection API exposes and how Kotlin code can also use Java reflection.
Why it matters: It clarifies why reflection-based JVM libraries can work with Kotlin classes. - Kotlin reflection dependency: State what dependency a project needs to use the Kotlin reflection API and why it is separate.
Why it matters: A project using Kotlin reflection must include its library explicitly.
Key terms: reflection, run time, serialization, Kotlin reflection API, Java reflection API, Java bytecode, kotlin-reflect.jar, org.jetbrains.kotlin:kotlin-reflect
12.2.1 The Kotlin reflection API: KClass, KCallable, KFunction, and KProperty
Introduces Kotlin reflection interfaces for inspecting classes and accessing functions and properties at run time.
- Class inspection with KClass: Explain how to obtain a KClass for a class or object and use it to inspect declarations, including inherited nonextension properties.
Why it matters: KClass lets you discover what an object exposes before accessing its members through reflection. - Reflective function calls and types: Distinguish the generic KCallable.call method from invoking a specifically typed KFunction, including how each handles arguments.
Why it matters: The choice between call and invoke determines whether argument mistakes are caught at compile time or run time. - Property getters through reflection: Describe how to retrieve top-level and member property values using the appropriate KProperty interface and its get method.
Why it matters: Reading a member property reflectively requires the particular object whose value you want. - Reflection interface hierarchy and limits: Describe how the reflection interfaces relate to annotated declarations and state which variables cannot be accessed by reflection here.
Why it matters: The hierarchy helps identify which reflective interface provides a capability and where reflection cannot be used.
Key terms: KClass, memberProperties, KCallable, KFunction, KFunction1, call, invoke, KProperty0, KProperty1, KMutableProperty0
12.2.2 Implementing object serialization using reflection
The section shows how JKid uses reflection to serialize an object’s properties into a JSON string.
- Reflective property serialization: Explain how serializeObject uses reflection to serialize an object’s properties and why reading those properties yields values of type Any?.
Why it matters: This explains how reflection lets the serializer handle properties whose types are not known in advance. - Default JSON value handling: Describe the JSON produced by default for an object’s properties and the roles of the helper functions in formatting it.
Why it matters: These default rules explain what JSON the reflective serializer produces before customization. - StringBuilder serialization structure: Trace how serialize produces its JSON string using buildString and a private StringBuilder extension.
Why it matters: This structure explains where the output is accumulated and why the implementation can make concise append calls.
Key terms: KProperty1<Any, *>, prop.get(obj), Any?, receiver, joinToStringBuilder, serializeString, serializePropertyValue, JSON arrays, nested objects, serialize
12.2.3 Customizing serialization with annotations
This section shows how JSON serialization uses reflection to honor annotations that exclude properties, rename JSON keys, and supply custom value serializers.
- Excluded property filtering: Explain how serialization omits properties marked with @JsonExclude by finding the annotation on reflected properties and filtering those properties out.
Why it matters: It explains how a declaration-level annotation changes which fields appear in the JSON output. - Annotated JSON property names: Describe how @JsonName determines a property's JSON key and what happens when the annotation is absent.
Why it matters: It shows how an annotation can alter an output key without changing the Kotlin property name. - Custom serializer selection: Trace how a property with @CustomSerializer obtains a serializer, including the difference between an object and a regular class, and how its value is written.
Why it matters: It connects the annotation's class argument to the serializer instance that converts a property's value.
Key terms: @JsonExclude, KProperty, KAnnotatedElement, findAnnotation, filter, @JsonName, prop.name, @CustomSerializer, getSerializer, ValueSerializer
12.2.4 JSON parsing and object deserialization
This section explains how JKid parses JSON and gathers values in seeds before constructing deserialized Kotlin objects.
- Reified deserialization type parameter: Explain why deserialize takes a reified type parameter and must be inline.
Why it matters: The type argument tells the deserializer what kind of object to produce. - JSON deserialization stages: Trace how the lexer, parser, and deserialization component turn JSON text into an object.
Why it matters: These stages separate reading characters, recognizing JSON structure, and building Kotlin values. - Parser callbacks for properties: Describe how the parser reports discovered JSON properties through JsonObject methods.
Why it matters: The callbacks connect recognized JSON structure to the object-building code. - Seed-based object construction: Explain why ObjectSeed stores constructor arguments during parsing and how spawn completes construction, including nested values.
Why it matters: Seeds let the deserializer gather constructor inputs, including nested objects, before it creates a data-class instance.
Key terms: deserialize, reified, inline, lexer, tokens, parser, deserializer, JsonObject, propertyName, createObject
12.2.5 The final step of deserialization: callBy() and creating objects using reflection
This section explains how JKid uses reflection to construct deserialized objects, convert JSON values to constructor parameter types, and cache the information needed to do so efficiently.
- Constructor invocation with defaults: Explain why deserialization uses KCallable.callBy instead of call and how its parameter-to-value map handles default arguments and JSON fields in any order.
Why it matters: JSON fields need not follow constructor order or repeat values the constructor already supplies. - Constructor argument type conversion: Explain how JKid determines and supplies values of the right types for reflected constructor parameters, including numeric values.
Why it matters: A JSON value must be converted to the constructor’s expected type before reflection can pass it safely. - Cached constructor annotation data: Describe why ClassInfoCache and ClassInfo cache the relationship between constructor parameters, properties, annotations, JSON names, and serializers.
Why it matters: Caching avoids repeating costly reflection while preserving annotation-driven deserialization behavior. - Required parameter validation: Distinguish how ClassInfo handles missing optional, nullable, and other constructor parameters before creating an object.
Why it matters: Checking omissions prevents construction with missing required data while permitting defaults and nullable values.
Key terms: KCallable.call, KCallable.callBy, default parameter values, KParameter.type, KType, ValueSerializer, @CustomSerializer, typeOf<>(), ClassInfoCache, ClassInfo
12.3 Summary
Where this is used later
- Using lambdas with receivers in HTML builders (chapter 13) · Introduces meta-annotations.
Concepts explained in this chapter
- reflectionReflection is a programming capability for inspecting or interacting with program types, members, values, or metadata dynamically at runtime.
- attributeAn annotation is a declaration-associated metadata construct whose availability and retention can be configured for source processing, compiled class files, or runtime access.
- deserializationDeserialization is the process of reconstructing objects or other in-memory data from a serialized representation.
- KPropertyKProperty is a Kotlin reflection interface that represents a property declaration and provides access to its reflected metadata and value.
- KClassKClass is the Kotlin reflection interface that represents a Kotlin class or object and exposes its runtime declarations and metadata.
- kotlin.reflectThe Kotlin reflection API provides runtime and compile-time facilities for inspecting Kotlin declarations, types, and callable members.
- ValueSerializerValueSerializer is a JKid interface for converting a Kotlin value to JSON and converting a JSON value back to that Kotlin type.
- KAnnotatedElementKAnnotatedElement is a Kotlin reflection interface implemented by runtime representations of declarations that can carry annotations.
- KProperty1KProperty1 is a Kotlin reflection interface for a property whose getter takes one receiver object of a specified type.
- get methodThe get method on a Kotlin property reference obtains the referenced property's value from a receiver object.
- annotation classAn annotation class is a Kotlin declaration that defines the structure and parameters of an annotation type without containing executable code.
- KFunctionKFunction is the Kotlin reflection interface representing a function declaration or callable function value.
- KCallableKCallable is a Kotlin reflection superinterface for callable functions and properties, including operations for invoking them.
- KCallable.callByKCallable.callBy is a Kotlin reflection API operation that invokes a callable using a map from parameters to argument values and can apply default parameter values.
- KTypeKType is a Kotlin reflection type representing a Kotlin type, including information such as nullability.
- lexical analysisA lexical analyzer converts an input character sequence into a list of tokens according to the input language's lexical rules.
- parsingParsing is the analysis of input according to structural or syntactic rules to identify its components and produce a structured, usable representation.
- reified type parameterA reified type parameter is an inline-function type parameter whose actual type information is available at runtime for restricted operations such as type checks and casts.
- tokenA token is a lexical unit representing either a syntactic symbol or a value in an input language.
- @CustomSerializer@CustomSerializer is a JKid annotation that associates a property with a custom value serializer.
- method invocationThe invoke method is a Kotlin function named invoke that enables instances of its containing type to be called with function-call syntax.
- JKidJKid is a book-specific pure Kotlin library that serializes and deserializes objects as JSON using annotations and reflection.
- KParameter.typeKParameter.type is a Kotlin reflection property that returns the reflected Kotlin type of a callable parameter.
- isMarkedNullableisMarkedNullable is a Kotlin reflection property that indicates whether a reflected type is declared nullable.
- isOptionalisOptional is a Kotlin reflection property that indicates whether a callable parameter has a default value and may be omitted.
- use-site targetA use-site target specifies which generated bytecode element receives an annotation when one Kotlin declaration produces multiple elements.
- serializationSerialization is the process of converting an object's state into a representation that can be stored or transmitted and later reconstructed.
- const qualifierThe const modifier declares a compile-time constant property of a permitted primitive type or String type.
- genericsA generic function is a function declared with one or more type parameters so it can operate on values of different types.
- star projectionA star projection is Kotlin syntax that represents a generic type argument whose specific type is unknown while preserving safe variance information.
- type argumentA type argument is an argument supplied to a parameterized construct to specify a type or constrain the kinds of values it accepts.
- named argumentNamed-argument syntax specifies function or constructor arguments by writing each argument's parameter name explicitly.
- accessor methodA property accessor is a getter or setter that defines how a Kotlin property value is read or changed.
- @JvmField@JvmField is a Kotlin annotation that exposes a property as a Java field instead of only through accessors.
- out keywordThe out modifier declares covariant use of a Kotlin type parameter so a parameterized type can accept subtype arguments.
These guides are generated from the text of Manning books by Manning's knowledge graph and link to the sections they draw on. The full books are available on liveBook and at manning.com.
