By default, ThreeTenABP.LocalDateTime is converted to
{"date":{"day":10,"month":4,"year":2018},"time":{"hour":3,"minute":34,"nano":115000000,"second":18}}
I can write an adapter to support ISO date string 2018-04-10T03:45:26.009
class LocalDateTimeAdapter {
#ToJson
fun toJson(value: LocalDateTime): String {
return FORMATTER.format(value)
}
#FromJson
fun fromJson(value: String): LocalDateTime {
return FORMATTER.parse(value, LocalDateTime.FROM)
}
companion object {
private val FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME
}
}
How can I write an adapter which can support both format (fromJson)
{"date":{"day":10,"month":4,"year":2018},"time":{"hour":3,"minute":34,"nano":115000000,"second":18}}
2018-04-10T03:45:26.009
Beside identifying which the format is used in fromJson, I am curious how Moshi internally perform toJson/fromJson for LocalDateTime
You’ll need to use JsonReader.peek() to determine the format of the incoming JSON, and then take action accordingly.
First install an adapter that converts LocalDateTime to a string. That adapter should use a qualifier annotation.
#Retention(RetentionPolicy.RUNTIME)
#JsonQualifier
#interface DateString {
}
Next create the string adapter. It should be straightforward, and might delegate to Moshi’s built-in Rfc3339DateJsonAdapter.
public final class LocalDateAsStringAdapter {
#ToJson String toJson(#DateString LocalDateTime localDateTime) {
...
}
#FromJson #DateString LocalDateTime fromJson(String string) {
...
}
}
Finally create an adapter that delegates either to Moshi’s built in adapter (that one will use {...}) or to your string adapter. This one prefers the string format, but you can do what you like.
public final class MultipleFormatsDateAdapter {
#ToJson void toJson(JsonWriter writer, LocalDateTime value,
#DateString JsonAdapter<LocalDateTime> stringAdapter) throws IOException {
stringAdapter.toJson(writer, value);
}
#FromJson LocalDateTime fromJson(JsonReader reader, #DateString JsonAdapter<LocalDateTime> stringAdapter,
JsonAdapter<LocalDateTime> defaultAdapter) throws IOException {
if (reader.peek() == JsonReader.Token.STRING) {
return stringAdapter.fromJson(reader);
} else {
return defaultAdapter.fromJson(reader);
}
}
}
This works because Moshi lets you declare multiple JsonAdapter arguments to the #ToJson and #FromJson methods, and these arguments may be annotated.
It also relies on the way this feature works if the types are the same. Here we’re making a JsonAdapter<LocalDateTime> by delegating to another JsonAdapter<LocalDateTime>. When the types are the same Moshi uses its nextAdapter() feature for composition.
Related
I am trying to read kafka messages from KafkaSpout and set tuple values from json that are parsed from that message. Actually, I am creating an additional Bolt that parses a tuple field called "value" with json string from KafkaSpout. Is it possible to set these values in Spout?
class ScanConfigKafkaSpout(kafkaUrl: String, kafkaGroup: String, kafkaTopic: String) : KafkaSpout<String, String>(
KafkaSpoutConfig
.builder(kafkaUrl, kafkaTopic)
.setProp(KEY_KAFKA_GROUP, "grp1")
.setProcessingGuarantee(KafkaSpoutConfig.ProcessingGuarantee.AT_MOST_ONCE)
.build()
), ComponentId {
override fun open(conf: MutableMap<String, Any>?, context: TopologyContext?, collector: SpoutOutputCollector?) {
try {
logger.debug("<${id()}> Opening ScanConfigKafkaSpout with ${conf.toString()}")
super.open(conf, context, collector)
logger.debug("<${id()}> ScanConfigKafkaSpout opened")
} catch (t: Throwable) {
logger.error("<${id()}> Error during opening CrawlScanConfigKafkaSpout", t)
}
}
override fun id(): String = SCAN_CONFIG_KAFKA_SPOUT
companion object {
private val logger = LoggerFactory.getLogger(ScanConfigKafkaSpout::class.java)
}
}
You probably need to implement the method declareOutputFields(OutputFieldsDeclarer declarer from IComponent.
It is used by Storm to serialize your attribute values and tuple configurations.
As stated here in the section Data Model , it says:
Every node in a topology must declare the output fields for the tuples it emits.
There is also a java example given for that method.
#Override
public void declareOutputFields(OutputFieldsDeclarer declarer) {
declarer.declare(new Fields("double", "triple"));
}
I have a class :
data class Stam(#SerializedName("blabla") val blabla: String = "")
I want to do gson.fromJson("{\"blabla\":null}", Stam::class.java)
However, it will fail because blabla is not nullable.
I want to make it so if gson failed to deserialize some variable, it will take the default value I give it.
How to achieve that?
I don't think it is possible with GSON, this is one of the reasons why kotlinx.serialization library was created. With this library it is fairly easy:
#Serializable
data class Stam(#SerialName("blabla") val blabla: String = "") //actually, #SerialName may be omitted if it is equal to field name
Json { coerceInputValues = true }.decodeFromString<Stam>("{\"blabla\":null}")
I wouldn't say it is not possible in Gson, but Gson is definitely not the best choice:
Gson has no mention on Kotlin, its runtime and specifics, so one is better to use a more convenient and Kotlin-aware tool. Typical questions here are: how to detect a data class (if it really matters, can be easily done in Kotlin), how to detect non-null parameters and fields in runtime, etc.
Data classes in Kotlin seem to provide a default constructor resolvable by Gson therefore Gson can invoke it (despite it can instantiate classes instances without constructors using unsafe mechanics) delegating to the "full-featured" constructor with the default arguments. The trick here is removing null-valued properties from input JSON so Gson would keep "default-argumented" fields unaffected.
I do Java but I do believe the following code can be converted easily (if you believe Gson is still a right choice):
final class StripNullTypeAdapterFactory
implements TypeAdapterFactory {
// The rule to check whether this type adapter should be applied.
// Externalizing the rule makes it much more flexible.
private final Predicate<? super TypeToken<?>> isClassSupported;
private StripNullTypeAdapterFactory(final Predicate<? super TypeToken<?>> isClassSupported) {
this.isClassSupported = isClassSupported;
}
static TypeAdapterFactory create(final Predicate<? super TypeToken<?>> isClassSupported) {
return new StripNullTypeAdapterFactory(isClassSupported);
}
#Override
#Nullable
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
if ( !isClassSupported.test(typeToken) ) {
return null;
}
// If the type is supported by the rule, get the type "real" delegate
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, typeToken);
return new StripNullTypeAdapter<>(delegate);
}
private static final class StripNullTypeAdapter<T>
extends TypeAdapter<T> {
private final TypeAdapter<T> delegate;
private StripNullTypeAdapter(final TypeAdapter<T> delegate) {
this.delegate = delegate;
}
#Override
public void write(final JsonWriter out, final T value)
throws IOException {
delegate.write(out, value);
}
#Override
public T read(final JsonReader in) {
// Another disadvantage in using Gson:
// the null-stripped object must be buffered into memory regardless how big it is.
// So it may generate really big memory footprints.
final JsonObject buffer = JsonParser.parseReader(in).getAsJsonObject();
// Strip null properties from the object
for ( final Iterator<Map.Entry<String, JsonElement>> i = buffer.entrySet().iterator(); i.hasNext(); ) {
final Map.Entry<String, JsonElement> property = i.next();
if ( property.getValue().isJsonNull() ) {
i.remove();
}
}
// Now there is no null values so Gson would only use properties appearing in the buffer
return delegate.fromJsonTree(buffer);
}
}
}
Test:
public final class StripNullTypeAdapterFactoryTest {
private static final Collection<Class<?>> supportedClasses = ImmutableSet.of(Stam.class);
private static final Gson gson = new GsonBuilder()
.disableHtmlEscaping()
// I don't know how easy detecting data classes and non-null parameters is
// but since the rule is externalized, let's just lookup it
// in the "known classes" registry
.registerTypeAdapterFactory(StripNullTypeAdapterFactory.create(typeToken -> supportedClasses.contains(typeToken.getRawType())))
.create();
#Test
public void test() {
final Stam stam = gson.fromJson("{\"blabla\":null}", Stam.class);
// The test is "green" since
Assertions.assertEquals("", stam.getBlabla());
}
}
I still think Gson is not the best choice here.
A JSON-API-response contains the following properties:
created_at_timestamp: 1565979486,
timezone: "+01:00",
I am using Moshi and ThreeTenBp to parse the time stamps and prepared the following custom adapters:
class ZonedDateTimeAdapter {
#FromJson
fun fromJson(jsonValue: Long?) = jsonValue?.let {
try {
ZonedDateTime.ofInstant(Instant.ofEpochSecond(jsonValue), ZoneOffset.UTC) // <---
} catch (e: DateTimeParseException) {
println(e.message)
null
}
}
}
As you can see the zone offset is hardcoded here.
class ZonedDateTimeJsonAdapter : JsonAdapter<ZonedDateTime>() {
private val delegate = ZonedDateTimeAdapter()
override fun fromJson(reader: JsonReader): ZonedDateTime? {
val jsonValue = reader.nextLong()
return delegate.fromJson(jsonValue)
}
}
...
class ZoneOffsetAdapter {
#FromJson
fun fromJson(jsonValue: String?) = jsonValue?.let {
try {
ZoneOffset.of(jsonValue)
} catch (e: DateTimeException) {
println(e.message)
null
}
}
}
...
class ZoneOffsetJsonAdapter : JsonAdapter<ZoneOffset>() {
private val delegate = ZoneOffsetAdapter()
override fun fromJson(reader: JsonReader): ZoneOffset? {
val jsonValue = reader.nextString()
return delegate.fromJson(jsonValue)
}
}
The adapters are registered with Moshi as follows:
Moshi.Builder()
.add(ZoneOffset::class.java, ZoneOffsetJsonAdapter())
.add(ZonedDateTime::class.java, ZonedDateTimeJsonAdapter())
.build()
Parsing the individual fields (created_at_timestamp, timezone) works fine. I want however get rid of the hardcoded zone offset. How can I configure Moshi to fall back on the timezone property when parsing the created_at_timestamp property.
Related
Advanced JSON parsing techniques using Moshi and Kotlin
The work-in-progress branch of the related project
For the created_at_timestamp field you should use a type that doesn't have a timezone. This is usually Instant. It identifies a moment in time independent of which timezone it is being interpreted in.
Then in your enclosing type you can define a getter method to combine the instant and zone into one value. The ZonedDateTime.ofInstant method can do this.
I am implementing a declarative client in Micronaut that looks like this:
#Get("/dostuff{?requestObject*}")
fun getStuff(requestObject: MyRequestObject): String
My MyRequestObject contains an enum that is represented by some string:
data class MyRequestObject(val myEnum: MyEnum)
enum class MyEnum(val stringRep: String) {
AREASONABLENAME("someSillyString");
}
When I now send a request via the client the value from requestObject generates the following query /?myEnum=AREASONABLENAME. What I actually need is /?myEnum=someSillyString.
I tried the following things without any success:
add JsonValue function to MyEnum:
#JsonValue fun getJsonValue() = stringRep - of course did not help
implement a TypeConverter for MyEnum
#Singleton
class MyEnumTypeConverter : TypeConverter<MyEnum, String> {
override fun convert(`object`: MyEnum?, targetType: Class<String>?, context: ConversionContext?): Optional<String> {
return Optional.ofNullable(`object`?.stringRep)
}
}
Is there a way to achieve the desired behaviour?
You can override the toString method in the Enum so that when the converter tries to convert it to a string you can control the result of the operation:
enum class MyEnum(val stringRep: String) {
AREASONABLENAME("someSillyString");
override fun toString(): String {
return stringRep
}
}
I'm trying to build a class that has a property of LocalDate type which has setters that accept different types: LocalDate or String. In case of LocalDate, the value gets assigned directly, in case of String, it gets parsed and then assigned.
In Java, I just need to implement two overloaded setters handling both of above mentioned cases. But I have no idea how to handle that in Kotlin. I have tried this:
class SomeExampleClass(var _date: LocalDate) {
var date = _date
set(value) {
when(value) {
is LocalDate -> value
is String -> LocalDate.parse(value)
}
}
}
It doesn't compile. How can I resolve such a problem?
After some time I returned to the problem of overloaded setters and developed the following solution:
class A(_date: LocalDate) {
var date: Any = _date
set(value) {
field = helperSet(value)
}
get() = field as LocalDate
private fun <T> helperSet(t: T) = when (t) {
is LocalDate -> t
is String -> LocalDate.parse(t)
else -> throw IllegalArgumentException()
}
}
So if you just want to construct it (via constructor), just create a secondary constructor
SomeExampleClass(LocalDate.MAX)
SomeExampleClass("2007-12-03")
class SomeExampleClass(var _date: LocalDate) {
constructor(_date: String) : this(LocalDate.parse(_date))
}