Closed nlochschmidt closed 7 years ago
Yes, known limitation - the TypeMapper function need to be defined for default values (zeros, empty strings) so we can generate a defaultInstance
. For these two specific examples, I would suggest using optional fields instead of required fields. For PosInteger - I'd avoid mixing validation logic with the (de)serialization logic. For the UUID, you can represent invalid UUIDs as a special value like new UUID(0,0)
@nlochschmidt A bit curious regarding your PosMoneyAmount
. Does it represent pennies?
In some of our projects we have a type case class Money(pennies: Int)
which is used with validated positive amounts. In order to represent negative amounts we use another type case class MoneyDelta(amount: Money, sign: Sign)
where Sign
is one of Positive
, Negative
and Zero
.
@ahjohannessen Actually, this is just a simplified example. In our application, we are having a SavingsRate
class which contains a custom MonetaryAmount
which itself contains a BigDecimal
value and a currency. Since it doesn't make sense for a savings rate to be negative and since we in addition only allow Euro values as well as integer values for the savings rate right now we have these constraints in our code.
@thesamet I agree that it is probably better to avoid mixing up validation with serialization logic, however I would really like to know why it's necessary to be able to create a defaultInstance
in the first place.
@nlochschmidt Say we have:
message A { ...things... }
message B { optional A a = 1; }
In the generated code, we want to be able to do B().getA
which would return the default instance of A
in the case where a
is unset.
@thesamet Ok, I didn't see those getX
methods yet, because I actually only used protobuf version 2 and only required fields. However, that design decision sounds extremely dangerous (as in bug inducing) to me. I would not expect that there exists a method like that but if it does I would really expect it to fail or return null
if the value is not set in the message. How is this supposed to be used? I guess you always have to guard it with some hasX
function, which does not seem to exist and which will probably defeat the whole purpose.
@nlochschmidt This is the same programming API that is available in the official Java/C++/Python code generators for protocol buffers. For example, see the "optional" section in https://developers.google.com/protocol-buffers/docs/javatutorial:
For embedded messages, the default value is always the "default instance" or "prototype" of the message, which has none of its fields set.
In ScalaPB, optional fields are implemented as Option[T]
values. I understand the expectation that getX
should behave as x.get
and would throw if the value is unset, though the choice here has been consistency with Java and other implementations. The hasX
guard in ScalaPB is just x.isDefined
An example where this is useful if you have nested optional messages and you want to check if some optional primitive is defined: x.getA.getB.getC.getD.e.isDefined" - this code would work when A, B, C, D are unset. This is a very common situation and achieving this without get
getXfunction ends up very verbose (either a bunch of
isDefined` checks, or a nested series of flatMaps or a for-loop).
Regarding use of required field - see the "Optional is forever" note on the link above. Required fields can hinder schema evolution, and optional fields should be preferred. In that case, the presence validation check should be in the application layer (instead of in the deserializer).
@thesamet I didn't know the part about the default instance from the java tutorial and from that standpoint it makes total sense to support the standard protobuf semantics instead of deviating from them. Thanks for the detailed explanation 👍
I've managed to workaround this by using protected val
+ def
:
syntax = "proto3";
package com.aaabramov.example;
import "scalapb/scalapb.proto";
message Deposit {
option (scalapb.message).extends = "com.aaabramov.example.DepositExt";
option (scalapb.message).companion_extends = "com.aaabramov.example.DepositCompanionExt";
string rawAmount = 1 [(scalapb.field).annotations = 'protected val'];
}
package com.aaabramov.example
trait DepositExt { this: Deposit =>
def amount: PosMoneyAmount = PosMoneyAmount(rawAmount)
}
trait DepositCompanionExt {
final def apply(amount: PosMoneyAmount): Deposit =
Deposit(amount.amount)
}
I believe the original problem reported on this issue has been resolved in ScalaPB 0.10.10 and 0.11.x - custom types do not require to be defined for empty values. The parsing code no longer relies on defaultInstance
.
@thesamet I'm not sure about the fix in 0.11.x, after upgrading to 0.11.4 I'm still facing the same error, where defaultInstance
is used for serialization:
[info] java.lang.NumberFormatException
[info] at java.base/java.math.BigDecimal.<init>(BigDecimal.java:625)
[info] at java.base/java.math.BigDecimal.<init>(BigDecimal.java:402)
[info] at java.base/java.math.BigDecimal.<init>(BigDecimal.java:835)
[info] at scala.math.BigDecimal$.exact(BigDecimal.scala:121)
[info] at scala.math.BigDecimal$.apply(BigDecimal.scala:244)
[info] at ***.BigDecimalTypeMapper$.$anonfun$stingToBigDecimalMapper$1(BigDecimalTypeMapper.scala:7)
[info] at scalapb.TypeMapper$$anon$1.toCustom(TypeMapper.scala:26)
[info] at ***.Money$.defaultInstance$lzycompute(CsBaseProto.scala:1835)
[info] at ***.Money$.defaultInstance(CsBaseProto.scala:1834)
[info] at ***.BillingInfo.__computeSerializedValue(CsBaseProto.scala:5218)
[info] at ***.BillingInfo.serializedSize(CsBaseProto.scala:5235)
@aludwiko - can you double check if you regenerated the code after the ScalaPB upgrade (sbt clean
may be necessary). If this persists, can you create an example project that demonstrates the issue and share it on github? You can use the ScalaPB sbt template to quickly generate a minimal project:
sbt new scalapb/scalapb-template.g8
@thesamet Actually it's quite easy to reproduce it, just run the Main from: https://github.com/aludwiko/scalapb-serialization
Thanks for putting together the example. It looks like this issue can be reproduced only when no_box
is also used - let's track this in #1198.
Given this proto definition:
and these TypeMappers
I expected these tests to work:
Instead I get this output:
Full example project at https://github.com/nlochschmidt/ScalaPB-CustomTypesBug
It looks like the issue originates in the
defaultInstance
method which seems to try and create aUUID
from an empty string and aPosMoneyAmount
from 0.Is this a known limitation of using custom types in ScalaPB?